FreeCAD

Форк
0
/
Help.py 
464 строки · 16.1 Кб
1
# -*- coding: utf-8 -*-
2

3
# ***************************************************************************
4
# *   Copyright (c) 2021 Yorik van Havre <yorik@uncreated.net>              *
5
# *                                                                         *
6
# *   This program is free software; you can redistribute it and/or modify  *
7
# *   it under the terms of the GNU Lesser General Public License (LGPL)    *
8
# *   as published by the Free Software Foundation; either version 2 of     *
9
# *   the License, or (at your option) any later version.                   *
10
# *   for detail see the LICENCE text file.                                 *
11
# *                                                                         *
12
# *   This program is distributed in the hope that it will be useful,       *
13
# *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
14
# *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
15
# *   GNU Library General Public License for more details.                  *
16
# *                                                                         *
17
# *   You should have received a copy of the GNU Library General Public     *
18
# *   License along with this program; if not, write to the Free Software   *
19
# *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  *
20
# *   USA                                                                   *
21
# *                                                                         *
22
# ***************************************************************************
23

24
"""
25
Provide tools to access the FreeCAD documentation.
26

27
The main usage is using the "show" function. It can retrieve an URL,
28
a local file (markdown or html), or find a page automatically from
29
the settings set under Preferences->General->Help.
30

31
It doesn't matter what you give, the system will recognize if the contents are
32
HTML or Markdown and render it appropriately.
33

34
Basic usage:
35

36
    import Help
37
    Help.show("Draft Line")
38
    Help.show("Draft_Line") # works with spaces or underscores
39
    Help.show("https://wiki.freecadweb.org/Draft_Line")
40
    Help.show("https://gitlab.com/freecad/FreeCAD-documentation/-/raw/main/wiki/Draft_Line.md")
41
    Help.show("/home/myUser/.FreeCAD/Documentation/Draft_Line.md")
42
    Help.show("http://myserver.com/myfolder/Draft_Line.html")
43

44
Preferences keys (in "User parameter:BaseApp/Preferences/Mod/Help"):
45

46
    optionBrowser/optionTab/optionDialog (bool): Where to open the help dialog
47
    optionOnline/optionOffline (bool): where to fetch the documentation from
48
    URL (string): online location
49
    Location (string): offline location
50
    Suffix (string): a suffix to add to the URL, ex: /fr
51
    StyleSheet (string): optional CSS stylesheet to style the output
52

53
Defaults are to open the wiki in the desktop browser
54
"""
55

56
import os
57
import FreeCAD
58

59

60
translate = FreeCAD.Qt.translate
61
QT_TRANSLATE_NOOP = FreeCAD.Qt.QT_TRANSLATE_NOOP
62

63
# texts and icons
64
WIKI_URL = "https://wiki.freecad.org"
65
MD_RAW_URL = "https://raw.githubusercontent.com/FreeCAD/FreeCAD-documentation/main/wiki"
66
MD_RENDERED_URL = "https://github.com/FreeCAD/FreeCAD-documentation/blob/main/wiki"
67
MD_TRANSLATIONS_FOLDER = "translations"
68
ERRORTXT = translate(
69
    "Help",
70
    "Contents for this page could not be retrieved. Please check settings under menu Edit -> Preferences -> General -> Help",
71
)
72
LOCTXT = translate(
73
    "Help",
74
    "Help files location could not be determined. Please check settings under menu Edit -> Preferences -> General -> Help",
75
)
76
LOGTXT = translate(
77
    "Help",
78
    "PySide QtWebEngineWidgets module is not available. Help rendering is done with the system browser",
79
)
80
CONVERTTXT = translate(
81
    "Help",
82
    "There is no Markdown renderer installed on your system, so this help page is rendered as is. Please install the Markdown or Pandoc Python modules to improve the rendering of this page.",
83
)
84
PREFS = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Help")
85
ICON = ":/icons/help-browser.svg"
86

87

88
def show(page, view=None, conv=None):
89
    """
90
    show(page,view=None, conv=None):
91
    Opens a help viewer and shows the given help page.
92
    The given help page can be a URL pointing to a markdown or HTML file,
93
    a name page / command name, or a file path pointing to a markdown
94
    or HTML file. If view is given (an instance of openBrowserHTML.HelpPage or
95
    any other object with a 'setHtml()' method), the page will be
96
    rendered there, otherwise a new tab/widget will be created according to
97
    preferences settings. If conv is given (markdown, pandoc, github, builtin or
98
    none), the corresponding markdown conversion method is used. Otherwise, the
99
    module will use the best available.
100
    In non-GUI mode, this function simply outputs the markdown or HTML text.
101
    """
102

103
    page = underscore_page(page)
104
    location = get_location(page)
105
    FreeCAD.Console.PrintLog("Help: opening " + location + "\n")
106
    if not location:
107
        FreeCAD.Console.PrintError(LOCTXT + "\n")
108
        return
109
    md = get_contents(location)
110
    html = convert(md, conv)
111
    baseurl = get_uri(location)
112
    pagename = os.path.basename(page.replace("_", " ").replace(".md", ""))
113
    title = translate("Help", "Help") + ": " + pagename
114
    if FreeCAD.GuiUp:
115
        if PREFS.GetBool("optionTab", False) and get_qtwebwidgets():
116
            # MDI tab
117
            show_tab(html, baseurl, title, view)
118
        elif PREFS.GetBool("optionDialog", False) and get_qtwebwidgets():
119
            # floating dock window
120
            show_dialog(html, baseurl, title, view)
121
        else:
122
            # desktop web browser - default
123
            show_browser(location)
124
    else:
125
        # console mode, we just print the output
126
        print(md)
127

128

129
def underscore_page(page):
130
    """change spaces by underscores in the given page name"""
131

132
    if "/" in page:
133
        page = page.split("/")
134
        page[-1] = page[-1].replace(" ", "_")
135
        page = "/".join(page)
136
    else:
137
        page.replace(" ", "_")
138
    return page
139

140

141
def get_uri(location):
142
    """returns a valid URI from a disk or network location"""
143

144
    baseurl = os.path.dirname(location) + "/"
145
    if baseurl.startswith("/"):  # unix path
146
        baseurl = "file://" + baseurl
147
    if baseurl[0].isupper() and (baseurl[1] == ":"):  # windows path
148
        baseurl = baseurl.replace("\\", "/")
149
        baseurl = "file:///" + baseurl
150
    return baseurl
151

152

153
def get_location(page):
154
    """retrieves the location (online or offline) of a given page"""
155

156
    location = ""
157
    if page.startswith("http"):
158
        return page
159
    if page.startswith("file://"):
160
        return page[7:]
161
    # offline location
162
    if os.path.exists(page):
163
        return page
164
    page = page.replace(".md", "")
165
    page = page.replace(" ", "_")
166
    page = page.replace("wiki/", "")
167
    page = page.split("#")[0]
168
    suffix = PREFS.GetString("Suffix", "")
169
    if suffix:
170
        if not suffix.startswith("/"):
171
            suffix = "/" + suffix
172
    if PREFS.GetBool("optionWiki", True):  # default
173
        location = WIKI_URL + "/" + page + suffix
174
    elif PREFS.GetBool("optionMarkdown", False):
175
        if PREFS.GetBool("optionBrowser", False):
176
            location = MD_RENDERED_URL
177
        else:
178
            location = MD_RAW_URL
179
        if suffix:
180
            location += "/" + MD_TRANSLATIONS_FOLDER + suffix
181
        location += "/" + page + ".md"
182
    elif PREFS.GetBool("optionGithub", False):
183
        location = MD_RENDERED_URL
184
        if suffix:
185
            location += "/" + MD_TRANSLATIONS_FOLDER + suffix
186
        location += "/" + page + ".md"
187
    elif PREFS.GetBool("optionCustom", False):
188
        location = PREFS.GetString("Location", "")
189
        if not location:
190
            location = os.path.join(
191
                FreeCAD.getUserAppDataDir(),
192
                "Mod",
193
                "offline-documentation",
194
                "FreeCAD-documentation-main",
195
                "wiki",
196
            )
197
        location = os.path.join(location, page + ".md")
198
    return location
199

200

201
def show_browser(url):
202
    """opens the desktop browser with the given URL"""
203

204
    from PySide import QtCore, QtGui
205

206
    try:
207
        ret = QtGui.QDesktopServices.openUrl(QtCore.QUrl(url))
208
        if not ret:
209
            # some users reported problems with the above
210
            import webbrowser
211

212
            webbrowser.open_new(url)
213
    except:
214
        import webbrowser
215

216
        webbrowser.open_new(url)
217

218

219
def show_dialog(html, baseurl, title, view=None):
220
    """opens a dock dialog with the given html"""
221

222
    from PySide import QtCore
223

224
    if view:  # reusing existing view
225
        view.setHtml(html, baseUrl=QtCore.QUrl(baseurl))
226
        view.parent().parent().setWindowTitle(title)
227
    else:
228
        openBrowserHTML(html, baseurl, title, ICON, dialog=True)
229

230

231
def show_tab(html, baseurl, title, view=None):
232
    """opens a MDI tab with the given html"""
233

234
    from PySide import QtCore
235

236
    if view:  # reusing existing view
237
        view.setHtml(html, baseUrl=QtCore.QUrl(baseurl))
238
        view.parent().parent().setWindowTitle(title)
239
    else:
240
        openBrowserHTML(html, baseurl, title, ICON)
241

242

243
def get_qtwebwidgets():
244
    """verifies if qtwebengine is available"""
245

246
    try:
247
        from PySide import QtWebEngineWidgets
248
    except:
249
        FreeCAD.Console.PrintLog(LOGTXT + "\n")
250
        return False
251
    else:
252
        return True
253

254

255
def get_contents(location):
256
    """retrieves text contents of a given page"""
257

258
    import urllib
259

260
    if location.startswith("http"):
261
        import urllib.request
262

263
        try:
264
            r = urllib.request.urlopen(location)
265
        except:
266
            return ERRORTXT
267
        contents = r.read().decode("utf8")
268
        return contents
269
    else:
270
        if os.path.exists(location):
271
            with open(location, mode="r", encoding="utf8") as f:
272
                contents = f.read()
273
            return contents
274
    return ERRORTXT
275

276

277
def convert(content, force=None):
278
    """converts the given markdown code to html. Force can be None (automatic)
279
    or markdown, pandoc, github or raw/builtin"""
280

281
    import urllib
282

283
    def convert_markdown(m):
284
        try:
285
            import markdown
286
            from markdown.extensions import codehilite
287

288
            return markdown.markdown(m, extensions=["codehilite"])
289
        except:
290
            return None
291

292
    def convert_pandoc(m):
293
        try:
294
            import pypandoc
295

296
            return pypandoc.convert_text(m, "html", format="md")
297
        except:
298
            return None
299

300
    def convert_github(m):
301
        try:
302
            import json
303
            import urllib.request
304

305
            data = {"text": m, "mode": "markdown"}
306
            bdata = json.dumps(data).encode("utf-8")
307
            return (
308
                urllib.request.urlopen("https://api.github.com/markdown", data=bdata)
309
                .read()
310
                .decode("utf8")
311
            )
312
        except:
313
            return None
314

315
    def convert_raw(m):
316
        # simple and dirty regex-based markdown to html
317

318
        import re
319

320
        f = re.DOTALL | re.MULTILINE
321
        m = re.sub(r"^##### (.*?)\n", r"<h5>\1</h5>\n", m, flags=f)  # ##### titles
322
        m = re.sub(r"^#### (.*?)\n", r"<h4>\1</h4>\n", m, flags=f)  # #### titles
323
        m = re.sub(r"^### (.*?)\n", r"<h3>\1</h3>\n", m, flags=f)  # ### titles
324
        m = re.sub(r"^## (.*?)\n", r"<h2>\1</h2>\n", m, flags=f)  # ## titles
325
        m = re.sub(r"^# (.*?)\n", r"<h1>\1</h1>\n", m, flags=f)  # # titles
326
        m = re.sub(r"!\[(.*?)\]\((.*?)\)", r'<img alt="\1" src="\2">', m, flags=f)  # images
327
        m = re.sub(r"\[(.*?)\]\((.*?)\)", r'<a href="\2">\1</a>', m, flags=f)  # links
328
        m = re.sub(r"\*\*(.*?)\*\*", r"<b>\1</b>", m)  # bold
329
        m = re.sub(r"\*(.*?)\*", r"<i>\1</i>", m)  # italic
330
        m = re.sub(r"\n\n", r"<br/>", m, flags=f)  # double new lines
331
        m += "\n<br/><hr/><small>" + CONVERTTXT + "</small>"
332
        return m
333

334
    if "<html" in content:
335
        # this is html already
336
        return content
337

338
    if force == "markdown":
339
        html = convert_markdown(content)
340
    elif force == "pandoc":
341
        html = convert_pandoc(content)
342
    elif force == "github":
343
        html = convert_github(content)
344
    elif force in ["raw", "builtin"]:
345
        html = convert_raw(content)
346
    elif force == "none":
347
        return content
348
    else:
349
        # auto mode
350
        html = convert_pandoc(content)
351
        if not html:
352
            html = convert_markdown(content)
353
            if not html:
354
                html = convert_raw(content)
355
    if not "<html" in html:
356
        html = (
357
            '<html>\n<head>\n<meta charset="utf-8"/>\n</head>\n<body>\n\n'
358
            + html
359
            + "</body>\n</html>"
360
        )
361
    # insert css
362
    css = None
363
    cssfile = PREFS.GetString("StyleSheet", "")
364
    if not cssfile:
365
        cssfile = os.path.join(os.path.dirname(__file__), "default.css")
366
    if False:  # linked CSS file
367
        # below code doesn't work in FreeCAD apparently because it prohibits cross-URL stuff
368
        cssfile = urllib.parse.urljoin("file:", urllib.request.pathname2url(cssfile))
369
        css = '<link rel="stylesheet" type="text/css" href="' + cssfile + '"/>'
370
    else:
371
        if os.path.exists(cssfile):
372
            with open(cssfile) as cf:
373
                css = cf.read()
374
            if css:
375
                css = "<style>\n" + css + "\n</style>"
376
        else:
377
            print("Debug: Help: Unable to open css file:", cssfile)
378
    if css:
379
        html = html.replace("</head>", css + "\n</head>")
380
    return html
381

382

383
def add_preferences_page():
384
    """adds the Help preferences page to the UI"""
385

386
    import FreeCADGui
387

388
    page = os.path.join(os.path.dirname(__file__), "dlgPreferencesHelp.ui")
389
    FreeCADGui.addPreferencePage(page, QT_TRANSLATE_NOOP("QObject", "General"))
390

391

392
def add_language_path():
393
    """registers the Help translations to FreeCAD"""
394

395
    import FreeCADGui
396
    import Help_rc
397

398
    FreeCADGui.addLanguagePath(":/translations")
399

400

401
def openBrowserHTML(html, baseurl, title, icon, dialog=False):
402
    """creates a browser view and adds it as a FreeCAD MDI tab or dockable dialog"""
403

404
    import FreeCADGui
405
    from PySide import QtCore, QtGui, QtWidgets, QtWebEngineWidgets
406

407
    # turn an int into a qt dock area
408
    def getDockArea(area):
409
        if area == 1:
410
            return QtCore.Qt.LeftDockWidgetArea
411
        elif area == 4:
412
            return QtCore.Qt.TopDockWidgetArea
413
        elif area == 8:
414
            return QtCore.Qt.BottomDockWidgetArea
415
        else:
416
            return QtCore.Qt.RightDockWidgetArea
417

418
    # save dock widget size and location
419
    def onDockLocationChanged(area):
420
        PREFS.SetInt("dockWidgetArea", int(area))
421
        mw = FreeCADGui.getMainWindow()
422
        dock = mw.findChild(QtWidgets.QDockWidget, "HelpWidget")
423
        if dock:
424
            PREFS.SetBool("dockWidgetFloat", dock.isFloating())
425
            PREFS.SetInt("dockWidgetWidth", dock.width())
426
            PREFS.SetInt("dockWidgetHeight", dock.height())
427

428
    # a custom page that handles .md links
429
    class HelpPage(QtWebEngineWidgets.QWebEnginePage):
430
        def acceptNavigationRequest(self, url, _type, isMainFrame):
431
            if _type == QtWebEngineWidgets.QWebEnginePage.NavigationTypeLinkClicked:
432
                show(url.toString(), view=self)
433
            return super().acceptNavigationRequest(url, _type, isMainFrame)
434

435
    mw = FreeCADGui.getMainWindow()
436
    view = QtWebEngineWidgets.QWebEngineView()
437
    page = HelpPage(None, view)
438
    page.setHtml(html, baseUrl=QtCore.QUrl(baseurl))
439
    view.setPage(page)
440

441
    if dialog:
442
        area = PREFS.GetInt("dockWidgetArea", 2)
443
        floating = PREFS.GetBool("dockWidgetFloat", True)
444
        height = PREFS.GetBool("dockWidgetWidth", 200)
445
        width = PREFS.GetBool("dockWidgetHeight", 300)
446
        dock = mw.findChild(QtWidgets.QDockWidget, "HelpWidget")
447
        if not dock:
448
            dock = QtWidgets.QDockWidget()
449
            dock.setObjectName("HelpWidget")
450
            mw.addDockWidget(getDockArea(area), dock)
451
            dock.setFloating(floating)
452
            dock.setGeometry(dock.x(), dock.y(), width, height)
453
            dock.dockLocationChanged.connect(onDockLocationChanged)
454
        dock.setWidget(view)
455
        dock.setWindowTitle(title)
456
        dock.setWindowIcon(QtGui.QIcon(icon))
457
        dock.show()
458
    else:
459
        mdi = mw.findChild(QtWidgets.QMdiArea)
460
        sw = mdi.addSubWindow(view)
461
        sw.setWindowTitle(title)
462
        sw.setWindowIcon(QtGui.QIcon(icon))
463
        sw.show()
464
        mdi.setActiveSubWindow(sw)
465

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

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

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

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