source: sasmodels/sasmodels/rst2html.py @ 1fbadb2

core_shell_microgelsmagnetic_modelticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since 1fbadb2 was 1fbadb2, checked in by Paul Kienzle <pkienzle@…>, 6 years ago

fix math rendering for rst2html: now need mathjax URL

  • Property mode set to 100644
File size: 8.0 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    from os.path import expanduser
145    with open(expanduser(filename)) as fid:
146        rst = fid.read()
147    html = rst2html(rst)
148    return html
149
150def wxview(html, url="", size=(850, 540)):
151    import wx
152    from wx.html2 import WebView
153    frame = wx.Frame(None, -1, size=size)
154    view = WebView.New(frame)
155    view.SetPage(html, url)
156    frame.Show()
157    return frame
158
159def view_html_wxapp(html, url=""):
160    import wx  # type: ignore
161    app = wx.App()
162    frame = wxview(html, url)
163    app.MainLoop()
164
165def view_url_wxapp(url):
166    import wx  # type: ignore
167    from wx.html2 import WebView
168    app = wx.App()
169    frame = wx.Frame(None, -1, size=(850, 540))
170    view = WebView.New(frame)
171    view.LoadURL(url)
172    frame.Show()
173    app.MainLoop()
174
175def qtview(html, url=""):
176    try:
177        from PyQt5.QtWebKitWidgets import QWebView
178        from PyQt5.QtCore import QUrl
179    except ImportError:
180        from PyQt4.QtWebKit import QWebView
181        from PyQt4.QtCore import QUrl
182    helpView = QWebView()
183    helpView.setHtml(html, QUrl(url))
184    helpView.show()
185    return helpView
186
187def view_html_qtapp(html, url=""):
188    import sys
189    try:
190        from PyQt5.QtWidgets import QApplication
191    except ImportError:
192        from PyQt4.QtGui import QApplication
193    app = QApplication([])
194    frame = qtview(html, url)
195    sys.exit(app.exec_())
196
197def view_url_qtapp(url):
198    import sys
199    try:
200        from PyQt5.QtWidgets import QApplication
201    except ImportError:
202        from PyQt4.QtGui import QApplication
203    app = QApplication([])
204    try:
205        from PyQt5.QtWebKitWidgets import QWebView
206        from PyQt5.QtCore import QUrl
207    except ImportError:
208        from PyQt4.QtWebKit import QWebView
209        from PyQt4.QtCore import QUrl
210    frame = QWebView()
211    frame.load(QUrl(url))
212    frame.show()
213    sys.exit(app.exec_())
214
215# Set default html viewer
216view_html = view_html_qtapp
217
218def can_use_qt():
219    """
220    Return True if QWebView exists.
221
222    Checks first in PyQt5 then in PyQt4
223    """
224    try:
225        from PyQt5.QtWebKitWidgets import QWebView
226        return True
227    except ImportError:
228        try:
229            from PyQt4.QtWebKit import QWebView
230            return True
231        except ImportError:
232            return False
233
234def view_help(filename, qt=False):
235    import os
236
237    if qt:
238        qt = can_use_qt()
239
240    url = "file:///"+os.path.abspath(filename).replace("\\", "/")
241    if filename.endswith('.rst'):
242        html = load_rst_as_html(filename)
243        if qt:
244            view_html_qtapp(html, url)
245        else:
246            view_html_wxapp(html, url)
247    else:
248        if qt:
249            view_url_qtapp(url)
250        else:
251            view_url_wxapp(url)
252
253def main():
254    import sys
255    view_help(sys.argv[1], qt=False)
256
257if __name__ == "__main__":
258    main()
Note: See TracBrowser for help on using the repository browser.