source: sasmodels/sasmodels/rst2html.py @ 6dccecc

ticket-1257-vesicle-productticket_1156ticket_822_more_unit_tests
Last change on this file since 6dccecc was b297ba9, checked in by Paul Kienzle <pkienzle@…>, 5 years ago

lint

  • Property mode set to 100644
File size: 8.7 KB
Line 
1r"""
2Convert a restructured text document to html.
3
4Inline math markup can uses the *math* directive, or it can use latex
5style *\$expression\$*.  Math can be rendered using simple html and
6unicode, or with mathjax.
7"""
8
9import re
10from contextlib import contextmanager
11
12# CRUFT: locale.getlocale() fails on some versions of OS X
13# See https://bugs.python.org/issue18378
14import locale
15if hasattr(locale, '_parse_localename'):
16    try:
17        locale._parse_localename('UTF-8')
18    except ValueError:
19        _old_parse_localename = locale._parse_localename
20        def _parse_localename(localename):
21            code = locale.normalize(localename)
22            if code == 'UTF-8':
23                return None, code
24            else:
25                return _old_parse_localename(localename)
26        locale._parse_localename = _parse_localename
27
28from docutils.core import publish_parts
29from docutils.writers.html4css1 import HTMLTranslator
30from docutils.nodes import SkipNode
31
32def rst2html(rst, part="whole", math_output="mathjax"):
33    r"""
34    Convert restructured text into simple html.
35
36    Valid *math_output* formats for formulas include:
37    - html
38    - mathml
39    - mathjax
40    See `<http://docutils.sourceforge.net/docs/user/config.html#math-output>`_
41    for details.
42
43    The following *part* choices are available:
44    - whole: the entire html document
45    - html_body: document division with title and contents and footer
46    - body: contents only
47
48    There are other parts, but they don't make sense alone:
49
50        subtitle, version, encoding, html_prolog, header, meta,
51        html_title, title, stylesheet, html_subtitle, html_body,
52        body, head, body_suffix, fragment, docinfo, html_head,
53        head_prefix, body_prefix, footer, body_pre_docinfo, whole
54    """
55    # Ick! mathjax doesn't work properly with math-output, and the
56    # others don't work properly with math_output!
57    if math_output == "mathjax":
58        # TODO: this is copied from docs/conf.py; there should be only one
59        mathjax_path = "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-MML-AM_CHTML"
60        settings = {"math_output": math_output + " " + mathjax_path}
61    else:
62        settings = {"math-output": math_output}
63
64    # TODO: support stylesheets
65    #html_root = "/full/path/to/_static/"
66    #sheets = [html_root+s for s in ["basic.css","classic.css"]]
67    #settings["embed_styesheet"] = True
68    #settings["stylesheet_path"] = sheets
69
70    # math2html and mathml do not support \frac12
71    rst = replace_compact_fraction(rst)
72
73    # mathml, html do not support \tfrac
74    if math_output in ("mathml", "html"):
75        rst = rst.replace(r'\tfrac', r'\frac')
76
77    rst = replace_dollar(rst)
78    with suppress_html_errors():
79        parts = publish_parts(source=rst, writer_name='html',
80                              settings_overrides=settings)
81    return parts[part]
82
83@contextmanager
84def suppress_html_errors():
85    r"""
86    Context manager for keeping error reports out of the generated HTML.
87
88    Within the context, system message nodes in the docutils parse tree
89    will be ignored.  After the context, the usual behaviour will be restored.
90    """
91    visit_system_message = HTMLTranslator.visit_system_message
92    HTMLTranslator.visit_system_message = _skip_node
93    yield None
94    HTMLTranslator.visit_system_message = visit_system_message
95
96def _skip_node(self, node):
97    raise SkipNode
98
99
100_compact_fraction = re.compile(r"(\\[cdt]?frac)([0-9])([0-9])")
101def replace_compact_fraction(content):
102    r"""
103    Convert \frac12 to \frac{1}{2} for broken latex parsers
104    """
105    return _compact_fraction.sub(r"\1{\2}{\3}", content)
106
107
108_dollar = re.compile(r"(?:^|(?<=\s|[-(]))[$]([^\n]*?)(?<![\\])[$](?:$|(?=\s|[-.,;:?\\)]))")
109_notdollar = re.compile(r"\\[$]")
110def replace_dollar(content):
111    r"""
112    Convert dollar signs to inline math markup in rst.
113    """
114    content = _dollar.sub(r":math:`\1`", content)
115    content = _notdollar.sub("$", content)
116    return content
117
118
119def test_dollar():
120    """
121    Test substitution of dollar signs with equivalent RST math markup
122    """
123    assert replace_dollar(u"no dollar") == u"no dollar"
124    assert replace_dollar(u"$only$") == u":math:`only`"
125    assert replace_dollar(u"$first$ is good") == u":math:`first` is good"
126    assert replace_dollar(u"so is $last$") == u"so is :math:`last`"
127    assert replace_dollar(u"and $mid$ too") == u"and :math:`mid` too"
128    assert replace_dollar(u"$first$, $mid$, $last$") == u":math:`first`, :math:`mid`, :math:`last`"
129    assert replace_dollar(u"dollar\\$ escape") == u"dollar$ escape"
130    assert replace_dollar(u"dollar \\$escape\\$ too") == u"dollar $escape$ too"
131    assert replace_dollar(u"spaces $in the$ math") == u"spaces :math:`in the` math"
132    assert replace_dollar(u"emb\\ $ed$\\ ed") == u"emb\\ :math:`ed`\\ ed"
133    assert replace_dollar(u"$first$a") == u"$first$a"
134    assert replace_dollar(u"a$last$") == u"a$last$"
135    assert replace_dollar(u"$37") == u"$37"
136    assert replace_dollar(u"($37)") == u"($37)"
137    assert replace_dollar(u"$37 - $43") == u"$37 - $43"
138    assert replace_dollar(u"($37, $38)") == u"($37, $38)"
139    assert replace_dollar(u"a $mid$dle a") == u"a $mid$dle a"
140    assert replace_dollar(u"a ($in parens$) a") == u"a (:math:`in parens`) a"
141    assert replace_dollar(u"a (again $in parens$) a") == u"a (again :math:`in parens`) a"
142
143def load_rst_as_html(filename):
144    # type: (str) -> str
145    """Load rst from file and convert to html"""
146    from os.path import expanduser
147    with open(expanduser(filename)) as fid:
148        rst = fid.read()
149    html = rst2html(rst)
150    return html
151
152def wxview(html, url="", size=(850, 540)):
153    # type: (str, str, Tuple[int, int]) -> "wx.Frame"
154    """View HTML in a wx dialog"""
155    import wx
156    from wx.html2 import WebView
157    frame = wx.Frame(None, -1, size=size)
158    view = WebView.New(frame)
159    view.SetPage(html, url)
160    frame.Show()
161    return frame
162
163def view_html_wxapp(html, url=""):
164    # type: (str, str) -> None
165    """HTML viewer app in wx"""
166    import wx  # type: ignore
167    app = wx.App()
168    frame = wxview(html, url)
169    app.MainLoop()
170
171def view_url_wxapp(url):
172    # type: (str) -> None
173    """URL viewer app in wx"""
174    import wx  # type: ignore
175    from wx.html2 import WebView
176    app = wx.App()
177    frame = wx.Frame(None, -1, size=(850, 540))
178    view = WebView.New(frame)
179    view.LoadURL(url)
180    frame.Show()
181    app.MainLoop()
182
183def qtview(html, url=""):
184    # type: (str, str) -> "QWebView"
185    """View HTML in a Qt dialog"""
186    try:
187        from PyQt5.QtWebKitWidgets import QWebView
188        from PyQt5.QtCore import QUrl
189    except ImportError:
190        from PyQt4.QtWebKit import QWebView
191        from PyQt4.QtCore import QUrl
192    helpView = QWebView()
193    helpView.setHtml(html, QUrl(url))
194    helpView.show()
195    return helpView
196
197def view_html_qtapp(html, url=""):
198    # type: (str, str) -> None
199    """HTML viewer app in Qt"""
200    import sys
201    try:
202        from PyQt5.QtWidgets import QApplication
203    except ImportError:
204        from PyQt4.QtGui import QApplication
205    app = QApplication([])
206    frame = qtview(html, url)
207    sys.exit(app.exec_())
208
209def view_url_qtapp(url):
210    # type: (str) -> None
211    """URL viewer app in Qt"""
212    import sys
213    try:
214        from PyQt5.QtWidgets import QApplication
215    except ImportError:
216        from PyQt4.QtGui import QApplication
217    app = QApplication([])
218    try:
219        from PyQt5.QtWebKitWidgets import QWebView
220        from PyQt5.QtCore import QUrl
221    except ImportError:
222        from PyQt4.QtWebKit import QWebView
223        from PyQt4.QtCore import QUrl
224    frame = QWebView()
225    frame.load(QUrl(url))
226    frame.show()
227    sys.exit(app.exec_())
228
229# Set default html viewer
230view_html = view_html_qtapp
231
232def can_use_qt():
233    # type: () -> bool
234    """
235    Return True if QWebView exists.
236
237    Checks first in PyQt5 then in PyQt4
238    """
239    try:
240        from PyQt5.QtWebKitWidgets import QWebView
241        return True
242    except ImportError:
243        try:
244            from PyQt4.QtWebKit import QWebView
245            return True
246        except ImportError:
247            return False
248
249def view_help(filename, qt=False):
250    # type: (str, bool) -> None
251    """View rst or html file.  If *qt* use q viewer, otherwise use wx."""
252    import os
253
254    if qt:
255        qt = can_use_qt()
256
257    url = "file:///"+os.path.abspath(filename).replace("\\", "/")
258    if filename.endswith('.rst'):
259        html = load_rst_as_html(filename)
260        if qt:
261            view_html_qtapp(html, url)
262        else:
263            view_html_wxapp(html, url)
264    else:
265        if qt:
266            view_url_qtapp(url)
267        else:
268            view_url_wxapp(url)
269
270def main():
271    # type: () -> None
272    """Command line interface to rst or html viewer."""
273    import sys
274    view_help(sys.argv[1], qt=False)
275
276if __name__ == "__main__":
277    main()
Note: See TracBrowser for help on using the repository browser.