# SPDX-License-Identifier: LGPL-2.1-or-later # *************************************************************************** # * * # * Copyright (c) 2022-2024 The FreeCAD Project Association AISBL * # * * # * This file is part of FreeCAD. * # * * # * FreeCAD is free software: you can redistribute it and/or modify it * # * under the terms of the GNU Lesser General Public License as * # * published by the Free Software Foundation, either version 2.1 of the * # * License, or (at your option) any later version. * # * * # * FreeCAD is distributed in the hope that it will be useful, but * # * WITHOUT ANY WARRANTY; without even the implied warranty of * # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * # * Lesser General Public License for more details. * # * * # * You should have received a copy of the GNU Lesser General Public * # * License along with FreeCAD. If not, see * # * . * # * * # *************************************************************************** import re import FreeCAD # Get whatever version of PySide we can try: import PySide # Use the FreeCAD wrapper except ImportError: try: import PySide6 # Outside FreeCAD, try Qt6 first PySide = PySide6 except ImportError: import PySide2 # Fall back to Qt5 (if this fails, Python will kill this module's import) PySide = PySide2 from PySide import QtCore, QtGui, QtWidgets from typing import Optional class WidgetReadmeBrowser(QtWidgets.QTextBrowser): """A QTextBrowser widget that emits signals for each requested image resource, allowing an external controller to load and re-deliver those images. Once all resources have been re-delivered, the original data is redisplayed with the images in-line. Call setUrl prior to calling setMarkdown or setHtml to ensure URLs are resolved correctly.""" load_resource = QtCore.Signal(str) # Str is a URL to a resource follow_link = QtCore.Signal(str) # Str is a URL to another page def __init__(self, parent: QtWidgets.QWidget = None): super().__init__(parent) self.image_map = {} self.url = "" self.stop = False self.setOpenExternalLinks(True) def setUrl(self, url: str): """Set the base URL of the page. Used to resolve relative URLs in the page source.""" self.url = url def setMarkdown(self, md: str): """Provides an optional fallback to the markdown library for older versions of Qt (prior to 5.15) that did not have native markdown support. Lacking that, plaintext is displayed.""" geometry = self.geometry() if hasattr(super(), "setMarkdown"): super().setMarkdown(self._clean_markdown(md)) else: try: import markdown html = markdown.markdown(md) self.setHtml(html) except ImportError: self.setText(md) FreeCAD.Console.Warning( "Qt < 5.15 and no `import markdown` -- falling back to plain text display\n" ) self.setGeometry(geometry) def _clean_markdown(self, md: str): # Remove some HTML tags ( for now just img and br, which are the most common offenders that break rendering ) br_re = re.compile(r"") img_re = re.compile(r"]+)(?:'|\").*?\/?>") cleaned = br_re.sub(r"\n", md) cleaned = img_re.sub(r"[html tag removed]", cleaned) return cleaned def set_resource(self, resource_url: str, image: Optional[QtGui.QImage]): """Once a resource has been fetched (or the fetch has failed), this method should be used to inform the widget that the resource has been loaded. Note that the incoming image is scaled to 97% of the widget width if it is larger than that.""" self.image_map[resource_url] = self._ensure_appropriate_width(image) def loadResource(self, resource_type: int, name: QtCore.QUrl) -> object: """Callback for resource loading. Called automatically by underlying Qt code when external resources are needed for rendering. In particular, here it is used to download and cache (in RAM) the images needed for the README and Wiki pages.""" if resource_type == QtGui.QTextDocument.ImageResource and not self.stop: full_url = self._create_full_url(name.toString()) if full_url not in self.image_map: self.load_resource.emit(full_url) self.image_map[full_url] = None return self.image_map[full_url] elif resource_type == QtGui.QTextDocument.MarkdownResource: self.follow_link.emit(name.toString()) return self.toMarkdown() elif resource_type == QtGui.QTextDocument.HtmlResource: self.follow_link.emit(name.toString()) return self.toHtml() return super().loadResource(resource_type, name) def _ensure_appropriate_width(self, image: QtGui.QImage) -> QtGui.QImage: ninety_seven_percent = self.width() * 0.97 if image.width() < ninety_seven_percent: return image return image.scaledToWidth(ninety_seven_percent) def _create_full_url(self, url: str) -> str: if url.startswith("http"): return url if not self.url: return url lhs, slash, _ = self.url.rpartition("/") return lhs + slash + url