FreeCAD

Форк
0
/
addonmanager_readme_controller.py 
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

26
import FreeCAD
27
from Addon import Addon
28
import addonmanager_utilities as utils
29

30
from enum import IntEnum, Enum, auto
31
from html.parser import HTMLParser
32
from typing import Optional
33

34
import NetworkManager
35

36
translate = FreeCAD.Qt.translate
37

38
from PySide import QtCore, QtGui
39

40

41
class ReadmeDataType(IntEnum):
42
    PlainText = 0
43
    Markdown = 1
44
    Html = 2
45

46

47
class ReadmeController(QtCore.QObject):
48

49
    """A class that can provide README data from an Addon, possibly loading external resources such
50
    as images"""
51

52
    def __init__(self, widget):
53
        super().__init__()
54
        NetworkManager.InitializeNetworkManager()
55
        NetworkManager.AM_NETWORK_MANAGER.completed.connect(self._download_completed)
56
        self.readme_request_index = 0
57
        self.resource_requests = {}
58
        self.resource_failures = []
59
        self.url = ""
60
        self.readme_data = None
61
        self.readme_data_type = None
62
        self.addon: Optional[Addon] = None
63
        self.stop = True
64
        self.widget = widget
65
        self.widget.load_resource.connect(self.loadResource)
66
        self.widget.follow_link.connect(self.follow_link)
67

68
    def set_addon(self, repo: Addon):
69
        """Set which Addon's information is displayed"""
70

71
        self.addon = repo
72
        self.stop = False
73
        self.readme_data = None
74
        if self.addon.repo_type == Addon.Kind.MACRO:
75
            self.url = self.addon.macro.wiki
76
            if not self.url:
77
                self.url = self.addon.macro.url
78
            if not self.url:
79
                self.widget.setText(
80
                    translate(
81
                        "AddonsInstaller",
82
                        "Loading info for {} from the FreeCAD Macro Recipes wiki...",
83
                    ).format(self.addon.display_name, self.url)
84
                )
85
                return
86
        else:
87
            self.url = utils.get_readme_url(repo)
88
        self.widget.setUrl(self.url)
89

90
        self.widget.setText(
91
            translate("AddonsInstaller", "Loading page for {} from {}...").format(
92
                self.addon.display_name, self.url
93
            )
94
        )
95
        self.readme_request_index = NetworkManager.AM_NETWORK_MANAGER.submit_unmonitored_get(
96
            self.url
97
        )
98

99
    def _download_completed(self, index: int, code: int, data: QtCore.QByteArray) -> None:
100
        """Callback for handling a completed README file download."""
101
        if index == self.readme_request_index:
102
            if code == 200:  # HTTP success
103
                self._process_package_download(data.data().decode("utf-8"))
104
            else:
105
                self.widget.setText(
106
                    translate(
107
                        "AddonsInstaller",
108
                        "Failed to download data from {} -- received response code {}.",
109
                    ).format(self.url, code)
110
                )
111
        elif index in self.resource_requests:
112
            if code == 200:
113
                self._process_resource_download(self.resource_requests[index], data.data())
114
            else:
115
                FreeCAD.Console.PrintLog(f"Failed to load {self.resource_requests[index]}\n")
116
                self.resource_failures.append(self.resource_requests[index])
117
            del self.resource_requests[index]
118
            if not self.resource_requests:
119
                if self.readme_data:
120
                    if self.readme_data_type == ReadmeDataType.Html:
121
                        self.widget.setHtml(self.readme_data)
122
                    elif self.readme_data_type == ReadmeDataType.Markdown:
123
                        self.widget.setMarkdown(self.readme_data)
124
                    else:
125
                        self.widget.setText(self.readme_data)
126
                else:
127
                    self.set_addon(self.addon)  # Trigger a reload of the page now with resources
128

129
    def _process_package_download(self, data: str):
130
        if self.addon.repo_type == Addon.Kind.MACRO:
131
            parser = WikiCleaner()
132
            parser.feed(data)
133
            self.readme_data = parser.final_html
134
            self.readme_data_type = ReadmeDataType.Html
135
            self.widget.setHtml(parser.final_html)
136
        else:
137
            self.readme_data = data
138
            self.readme_data_type = ReadmeDataType.Markdown
139
            self.widget.setMarkdown(data)
140

141
    def _process_resource_download(self, resource_name: str, resource_data: bytes):
142
        image = QtGui.QImage.fromData(resource_data)
143
        self.widget.set_resource(resource_name, image)
144

145
    def loadResource(self, full_url: str):
146
        if full_url not in self.resource_failures:
147
            index = NetworkManager.AM_NETWORK_MANAGER.submit_unmonitored_get(full_url)
148
            self.resource_requests[index] = full_url
149

150
    def cancel_resource_loading(self):
151
        self.stop = True
152
        for request in self.resource_requests:
153
            NetworkManager.AM_NETWORK_MANAGER.abort(request)
154
        self.resource_requests.clear()
155

156
    def follow_link(self, url: str) -> None:
157
        final_url = url
158
        if not url.startswith("http"):
159
            if url.endswith(".md"):
160
                final_url = self._create_markdown_url(url)
161
            else:
162
                final_url = self._create_full_url(url)
163
        FreeCAD.Console.PrintLog(f"Loading {final_url} in the system browser")
164
        QtGui.QDesktopServices.openUrl(final_url)
165

166
    def _create_full_url(self, url: str) -> str:
167
        if url.startswith("http"):
168
            return url
169
        if not self.url:
170
            return url
171
        lhs, slash, _ = self.url.rpartition("/")
172
        return lhs + slash + url
173

174
    def _create_markdown_url(self, file: str) -> str:
175
        base_url = utils.get_readme_html_url(self.addon)
176
        lhs, slash, _ = base_url.rpartition("/")
177
        return lhs + slash + file
178

179

180
class WikiCleaner(HTMLParser):
181
    """This HTML parser cleans up FreeCAD Macro Wiki Page for display in a
182
    QTextBrowser widget (which does not deal will with tables used as formatting,
183
    etc.) It strips out any tables, and extracts the mw-parser-output div as the only
184
    thing that actually gets displayed. It also discards anything inside the [edit]
185
    spans that litter wiki output."""
186

187
    class State(Enum):
188
        BeforeMacroContent = auto()
189
        InMacroContent = auto()
190
        InTable = auto()
191
        InEditSpan = auto()
192
        AfterMacroContent = auto()
193

194
    def __init__(self):
195
        super().__init__()
196
        self.depth_in_div = 0
197
        self.depth_in_span = 0
198
        self.depth_in_table = 0
199
        self.final_html = "<html><body>"
200
        self.previous_state = WikiCleaner.State.BeforeMacroContent
201
        self.state = WikiCleaner.State.BeforeMacroContent
202

203
    def handle_starttag(self, tag: str, attrs):
204
        if tag == "div":
205
            self.handle_div_start(attrs)
206
        elif tag == "span":
207
            self.handle_span_start(attrs)
208
        elif tag == "table":
209
            self.handle_table_start(attrs)
210
        else:
211
            if self.state == WikiCleaner.State.InMacroContent:
212
                self.add_tag_to_html(tag, attrs)
213

214
    def handle_div_start(self, attrs):
215
        for name, value in attrs:
216
            if name == "class" and value == "mw-parser-output":
217
                self.previous_state = self.state
218
                self.state = WikiCleaner.State.InMacroContent
219
        if self.state == WikiCleaner.State.InMacroContent:
220
            self.depth_in_div += 1
221
            self.add_tag_to_html("div", attrs)
222

223
    def handle_span_start(self, attrs):
224
        for name, value in attrs:
225
            if name == "class" and value == "mw-editsection":
226
                self.previous_state = self.state
227
                self.state = WikiCleaner.State.InEditSpan
228
                break
229
        if self.state == WikiCleaner.State.InEditSpan:
230
            self.depth_in_span += 1
231
        elif WikiCleaner.State.InMacroContent:
232
            self.add_tag_to_html("span", attrs)
233

234
    def handle_table_start(self, unused):
235
        if self.state != WikiCleaner.State.InTable:
236
            self.previous_state = self.state
237
            self.state = WikiCleaner.State.InTable
238
        self.depth_in_table += 1
239

240
    def add_tag_to_html(self, tag, attrs=None):
241
        self.final_html += f"<{tag}"
242
        if attrs:
243
            self.final_html += " "
244
            for attr, value in attrs:
245
                self.final_html += f"{attr}='{value}'"
246
        self.final_html += ">\n"
247

248
    def handle_endtag(self, tag):
249
        if tag == "table":
250
            self.handle_table_end()
251
        elif tag == "span":
252
            self.handle_span_end()
253
        elif tag == "div":
254
            self.handle_div_end()
255
        else:
256
            if self.state == WikiCleaner.State.InMacroContent:
257
                self.add_tag_to_html(f"/{tag}")
258

259
    def handle_span_end(self):
260
        if self.state == WikiCleaner.State.InEditSpan:
261
            self.depth_in_span -= 1
262
            if self.depth_in_span <= 0:
263
                self.depth_in_span = 0
264
                self.state = self.previous_state
265
        else:
266
            self.add_tag_to_html(f"/span")
267

268
    def handle_div_end(self):
269
        if self.state == WikiCleaner.State.InMacroContent:
270
            self.depth_in_div -= 1
271
            if self.depth_in_div <= 0:
272
                self.depth_in_div = 0
273
                self.state = WikiCleaner.State.AfterMacroContent
274
                self.final_html += "</body></html>"
275
        else:
276
            self.add_tag_to_html(f"/div")
277

278
    def handle_table_end(self):
279
        if self.state == WikiCleaner.State.InTable:
280
            self.depth_in_table -= 1
281
            if self.depth_in_table <= 0:
282
                self.depth_in_table = 0
283
                self.state = self.previous_state
284

285
    def handle_data(self, data):
286
        if self.state == WikiCleaner.State.InMacroContent:
287
            self.final_html += data
288

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.