3
# ***************************************************************************
4
# * Copyright (c) 2021 Yorik van Havre <yorik@uncreated.net> *
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. *
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. *
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 *
22
# ***************************************************************************
25
Provide tools to access the FreeCAD documentation.
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.
31
It doesn't matter what you give, the system will recognize if the contents are
32
HTML or Markdown and render it appropriately.
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")
44
Preferences keys (in "User parameter:BaseApp/Preferences/Mod/Help"):
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
53
Defaults are to open the wiki in the desktop browser
60
translate = FreeCAD.Qt.translate
61
QT_TRANSLATE_NOOP = FreeCAD.Qt.QT_TRANSLATE_NOOP
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"
70
"Contents for this page could not be retrieved. Please check settings under menu Edit -> Preferences -> General -> Help",
74
"Help files location could not be determined. Please check settings under menu Edit -> Preferences -> General -> Help",
78
"PySide QtWebEngineWidgets module is not available. Help rendering is done with the system browser",
80
CONVERTTXT = translate(
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.",
84
PREFS = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Help")
85
ICON = ":/icons/help-browser.svg"
88
def show(page, view=None, conv=None):
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.
103
page = underscore_page(page)
104
location = get_location(page)
105
FreeCAD.Console.PrintLog("Help: opening " + location + "\n")
107
FreeCAD.Console.PrintError(LOCTXT + "\n")
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
115
if PREFS.GetBool("optionTab", False) and get_qtwebwidgets():
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)
122
# desktop web browser - default
123
show_browser(location)
125
# console mode, we just print the output
129
def underscore_page(page):
130
"""change spaces by underscores in the given page name"""
133
page = page.split("/")
134
page[-1] = page[-1].replace(" ", "_")
135
page = "/".join(page)
137
page.replace(" ", "_")
141
def get_uri(location):
142
"""returns a valid URI from a disk or network location"""
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
153
def get_location(page):
154
"""retrieves the location (online or offline) of a given page"""
157
if page.startswith("http"):
159
if page.startswith("file://"):
162
if os.path.exists(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", "")
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
178
location = MD_RAW_URL
180
location += "/" + MD_TRANSLATIONS_FOLDER + suffix
181
location += "/" + page + ".md"
182
elif PREFS.GetBool("optionGithub", False):
183
location = MD_RENDERED_URL
185
location += "/" + MD_TRANSLATIONS_FOLDER + suffix
186
location += "/" + page + ".md"
187
elif PREFS.GetBool("optionCustom", False):
188
location = PREFS.GetString("Location", "")
190
location = os.path.join(
191
FreeCAD.getUserAppDataDir(),
193
"offline-documentation",
194
"FreeCAD-documentation-main",
197
location = os.path.join(location, page + ".md")
201
def show_browser(url):
202
"""opens the desktop browser with the given URL"""
204
from PySide import QtCore, QtGui
207
ret = QtGui.QDesktopServices.openUrl(QtCore.QUrl(url))
209
# some users reported problems with the above
212
webbrowser.open_new(url)
216
webbrowser.open_new(url)
219
def show_dialog(html, baseurl, title, view=None):
220
"""opens a dock dialog with the given html"""
222
from PySide import QtCore
224
if view: # reusing existing view
225
view.setHtml(html, baseUrl=QtCore.QUrl(baseurl))
226
view.parent().parent().setWindowTitle(title)
228
openBrowserHTML(html, baseurl, title, ICON, dialog=True)
231
def show_tab(html, baseurl, title, view=None):
232
"""opens a MDI tab with the given html"""
234
from PySide import QtCore
236
if view: # reusing existing view
237
view.setHtml(html, baseUrl=QtCore.QUrl(baseurl))
238
view.parent().parent().setWindowTitle(title)
240
openBrowserHTML(html, baseurl, title, ICON)
243
def get_qtwebwidgets():
244
"""verifies if qtwebengine is available"""
247
from PySide import QtWebEngineWidgets
249
FreeCAD.Console.PrintLog(LOGTXT + "\n")
255
def get_contents(location):
256
"""retrieves text contents of a given page"""
260
if location.startswith("http"):
261
import urllib.request
264
r = urllib.request.urlopen(location)
267
contents = r.read().decode("utf8")
270
if os.path.exists(location):
271
with open(location, mode="r", encoding="utf8") as f:
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"""
283
def convert_markdown(m):
286
from markdown.extensions import codehilite
288
return markdown.markdown(m, extensions=["codehilite"])
292
def convert_pandoc(m):
296
return pypandoc.convert_text(m, "html", format="md")
300
def convert_github(m):
303
import urllib.request
305
data = {"text": m, "mode": "markdown"}
306
bdata = json.dumps(data).encode("utf-8")
308
urllib.request.urlopen("https://api.github.com/markdown", data=bdata)
316
# simple and dirty regex-based markdown to html
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>"
334
if "<html" in content:
335
# this is html already
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":
350
html = convert_pandoc(content)
352
html = convert_markdown(content)
354
html = convert_raw(content)
355
if not "<html" in html:
357
'<html>\n<head>\n<meta charset="utf-8"/>\n</head>\n<body>\n\n'
363
cssfile = PREFS.GetString("StyleSheet", "")
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 + '"/>'
371
if os.path.exists(cssfile):
372
with open(cssfile) as cf:
375
css = "<style>\n" + css + "\n</style>"
377
print("Debug: Help: Unable to open css file:", cssfile)
379
html = html.replace("</head>", css + "\n</head>")
383
def add_preferences_page():
384
"""adds the Help preferences page to the UI"""
388
page = os.path.join(os.path.dirname(__file__), "dlgPreferencesHelp.ui")
389
FreeCADGui.addPreferencePage(page, QT_TRANSLATE_NOOP("QObject", "General"))
392
def add_language_path():
393
"""registers the Help translations to FreeCAD"""
398
FreeCADGui.addLanguagePath(":/translations")
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"""
405
from PySide import QtCore, QtGui, QtWidgets, QtWebEngineWidgets
407
# turn an int into a qt dock area
408
def getDockArea(area):
410
return QtCore.Qt.LeftDockWidgetArea
412
return QtCore.Qt.TopDockWidgetArea
414
return QtCore.Qt.BottomDockWidgetArea
416
return QtCore.Qt.RightDockWidgetArea
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")
424
PREFS.SetBool("dockWidgetFloat", dock.isFloating())
425
PREFS.SetInt("dockWidgetWidth", dock.width())
426
PREFS.SetInt("dockWidgetHeight", dock.height())
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)
435
mw = FreeCADGui.getMainWindow()
436
view = QtWebEngineWidgets.QWebEngineView()
437
page = HelpPage(None, view)
438
page.setHtml(html, baseUrl=QtCore.QUrl(baseurl))
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")
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)
455
dock.setWindowTitle(title)
456
dock.setWindowIcon(QtGui.QIcon(icon))
459
mdi = mw.findChild(QtWidgets.QMdiArea)
460
sw = mdi.addSubWindow(view)
461
sw.setWindowTitle(title)
462
sw.setWindowIcon(QtGui.QIcon(icon))
464
mdi.setActiveSubWindow(sw)