FreeCAD
287 строк · 11.2 Кб
1# SPDX-License-Identifier: LGPL-2.1-or-later
2# ***************************************************************************
3# * *
4# * Copyright (c) 2024 The FreeCAD Project Association AISBL *
5# * *
6# * This file is part of FreeCAD. *
7# * *
8# * FreeCAD is free software: you can redistribute it and/or modify it *
9# * under the terms of the GNU Lesser General Public License as *
10# * published by the Free Software Foundation, either version 2.1 of the *
11# * License, or (at your option) any later version. *
12# * *
13# * FreeCAD is distributed in the hope that it will be useful, but *
14# * WITHOUT ANY WARRANTY; without even the implied warranty of *
15# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
16# * Lesser General Public License for more details. *
17# * *
18# * You should have received a copy of the GNU Lesser General Public *
19# * License along with FreeCAD. If not, see *
20# * <https://www.gnu.org/licenses/>. *
21# * *
22# ***************************************************************************
23
24""" A Qt Widget for displaying Addon README information """
25
26import FreeCAD27from Addon import Addon28import addonmanager_utilities as utils29
30from enum import IntEnum, Enum, auto31from html.parser import HTMLParser32from typing import Optional33
34import NetworkManager35
36translate = FreeCAD.Qt.translate37
38from PySide import QtCore, QtGui39
40
41class ReadmeDataType(IntEnum):42PlainText = 043Markdown = 144Html = 245
46
47class ReadmeController(QtCore.QObject):48
49"""A class that can provide README data from an Addon, possibly loading external resources such50as images"""
51
52def __init__(self, widget):53super().__init__()54NetworkManager.InitializeNetworkManager()55NetworkManager.AM_NETWORK_MANAGER.completed.connect(self._download_completed)56self.readme_request_index = 057self.resource_requests = {}58self.resource_failures = []59self.url = ""60self.readme_data = None61self.readme_data_type = None62self.addon: Optional[Addon] = None63self.stop = True64self.widget = widget65self.widget.load_resource.connect(self.loadResource)66self.widget.follow_link.connect(self.follow_link)67
68def set_addon(self, repo: Addon):69"""Set which Addon's information is displayed"""70
71self.addon = repo72self.stop = False73self.readme_data = None74if self.addon.repo_type == Addon.Kind.MACRO:75self.url = self.addon.macro.wiki76if not self.url:77self.url = self.addon.macro.url78if not self.url:79self.widget.setText(80translate(81"AddonsInstaller",82"Loading info for {} from the FreeCAD Macro Recipes wiki...",83).format(self.addon.display_name, self.url)84)85return86else:87self.url = utils.get_readme_url(repo)88self.widget.setUrl(self.url)89
90self.widget.setText(91translate("AddonsInstaller", "Loading page for {} from {}...").format(92self.addon.display_name, self.url93)94)95self.readme_request_index = NetworkManager.AM_NETWORK_MANAGER.submit_unmonitored_get(96self.url97)98
99def _download_completed(self, index: int, code: int, data: QtCore.QByteArray) -> None:100"""Callback for handling a completed README file download."""101if index == self.readme_request_index:102if code == 200: # HTTP success103self._process_package_download(data.data().decode("utf-8"))104else:105self.widget.setText(106translate(107"AddonsInstaller",108"Failed to download data from {} -- received response code {}.",109).format(self.url, code)110)111elif index in self.resource_requests:112if code == 200:113self._process_resource_download(self.resource_requests[index], data.data())114else:115FreeCAD.Console.PrintLog(f"Failed to load {self.resource_requests[index]}\n")116self.resource_failures.append(self.resource_requests[index])117del self.resource_requests[index]118if not self.resource_requests:119if self.readme_data:120if self.readme_data_type == ReadmeDataType.Html:121self.widget.setHtml(self.readme_data)122elif self.readme_data_type == ReadmeDataType.Markdown:123self.widget.setMarkdown(self.readme_data)124else:125self.widget.setText(self.readme_data)126else:127self.set_addon(self.addon) # Trigger a reload of the page now with resources128
129def _process_package_download(self, data: str):130if self.addon.repo_type == Addon.Kind.MACRO:131parser = WikiCleaner()132parser.feed(data)133self.readme_data = parser.final_html134self.readme_data_type = ReadmeDataType.Html135self.widget.setHtml(parser.final_html)136else:137self.readme_data = data138self.readme_data_type = ReadmeDataType.Markdown139self.widget.setMarkdown(data)140
141def _process_resource_download(self, resource_name: str, resource_data: bytes):142image = QtGui.QImage.fromData(resource_data)143self.widget.set_resource(resource_name, image)144
145def loadResource(self, full_url: str):146if full_url not in self.resource_failures:147index = NetworkManager.AM_NETWORK_MANAGER.submit_unmonitored_get(full_url)148self.resource_requests[index] = full_url149
150def cancel_resource_loading(self):151self.stop = True152for request in self.resource_requests:153NetworkManager.AM_NETWORK_MANAGER.abort(request)154self.resource_requests.clear()155
156def follow_link(self, url: str) -> None:157final_url = url158if not url.startswith("http"):159if url.endswith(".md"):160final_url = self._create_markdown_url(url)161else:162final_url = self._create_full_url(url)163FreeCAD.Console.PrintLog(f"Loading {final_url} in the system browser")164QtGui.QDesktopServices.openUrl(final_url)165
166def _create_full_url(self, url: str) -> str:167if url.startswith("http"):168return url169if not self.url:170return url171lhs, slash, _ = self.url.rpartition("/")172return lhs + slash + url173
174def _create_markdown_url(self, file: str) -> str:175base_url = utils.get_readme_html_url(self.addon)176lhs, slash, _ = base_url.rpartition("/")177return lhs + slash + file178
179
180class WikiCleaner(HTMLParser):181"""This HTML parser cleans up FreeCAD Macro Wiki Page for display in a182QTextBrowser widget (which does not deal will with tables used as formatting,
183etc.) It strips out any tables, and extracts the mw-parser-output div as the only
184thing that actually gets displayed. It also discards anything inside the [edit]
185spans that litter wiki output."""
186
187class State(Enum):188BeforeMacroContent = auto()189InMacroContent = auto()190InTable = auto()191InEditSpan = auto()192AfterMacroContent = auto()193
194def __init__(self):195super().__init__()196self.depth_in_div = 0197self.depth_in_span = 0198self.depth_in_table = 0199self.final_html = "<html><body>"200self.previous_state = WikiCleaner.State.BeforeMacroContent201self.state = WikiCleaner.State.BeforeMacroContent202
203def handle_starttag(self, tag: str, attrs):204if tag == "div":205self.handle_div_start(attrs)206elif tag == "span":207self.handle_span_start(attrs)208elif tag == "table":209self.handle_table_start(attrs)210else:211if self.state == WikiCleaner.State.InMacroContent:212self.add_tag_to_html(tag, attrs)213
214def handle_div_start(self, attrs):215for name, value in attrs:216if name == "class" and value == "mw-parser-output":217self.previous_state = self.state218self.state = WikiCleaner.State.InMacroContent219if self.state == WikiCleaner.State.InMacroContent:220self.depth_in_div += 1221self.add_tag_to_html("div", attrs)222
223def handle_span_start(self, attrs):224for name, value in attrs:225if name == "class" and value == "mw-editsection":226self.previous_state = self.state227self.state = WikiCleaner.State.InEditSpan228break229if self.state == WikiCleaner.State.InEditSpan:230self.depth_in_span += 1231elif WikiCleaner.State.InMacroContent:232self.add_tag_to_html("span", attrs)233
234def handle_table_start(self, unused):235if self.state != WikiCleaner.State.InTable:236self.previous_state = self.state237self.state = WikiCleaner.State.InTable238self.depth_in_table += 1239
240def add_tag_to_html(self, tag, attrs=None):241self.final_html += f"<{tag}"242if attrs:243self.final_html += " "244for attr, value in attrs:245self.final_html += f"{attr}='{value}'"246self.final_html += ">\n"247
248def handle_endtag(self, tag):249if tag == "table":250self.handle_table_end()251elif tag == "span":252self.handle_span_end()253elif tag == "div":254self.handle_div_end()255else:256if self.state == WikiCleaner.State.InMacroContent:257self.add_tag_to_html(f"/{tag}")258
259def handle_span_end(self):260if self.state == WikiCleaner.State.InEditSpan:261self.depth_in_span -= 1262if self.depth_in_span <= 0:263self.depth_in_span = 0264self.state = self.previous_state265else:266self.add_tag_to_html(f"/span")267
268def handle_div_end(self):269if self.state == WikiCleaner.State.InMacroContent:270self.depth_in_div -= 1271if self.depth_in_div <= 0:272self.depth_in_div = 0273self.state = WikiCleaner.State.AfterMacroContent274self.final_html += "</body></html>"275else:276self.add_tag_to_html(f"/div")277
278def handle_table_end(self):279if self.state == WikiCleaner.State.InTable:280self.depth_in_table -= 1281if self.depth_in_table <= 0:282self.depth_in_table = 0283self.state = self.previous_state284
285def handle_data(self, data):286if self.state == WikiCleaner.State.InMacroContent:287self.final_html += data288