1# This file is part of the Frescobaldi project, http://www.frescobaldi.org/
2#
3# Copyright (c) 2013 - 2014 by Wilbert Berendsen
4#
5# This program is free software; you can redistribute it and/or
6# modify it under the terms of the GNU General Public License
7# as published by the Free Software Foundation; either version 2
8# of the License, or (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program; if not, write to the Free Software
17# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
18# See http://www.gnu.org/licenses/ for more information.
19
20"""
21Page, a page from the Frescobaldi User Manual.
22"""
23
24
25import re
26
27from PyQt5.QtCore import QSettings
28from PyQt5.QtGui import QKeySequence
29
30import simplemarkdown
31
32from . import read
33from . import resolve
34
35
36class Page(object):
37    def __init__(self, name=None):
38        self._attrs = {}
39        self._title = None
40        self._body = None
41        self._name = None
42        if name:
43            self.load(name)
44
45    def load(self, name):
46        """Parse and translate the named document."""
47        self._name = name
48        try:
49            doc, attrs = read.document(name)
50        except (OSError, IOError):
51            doc, attrs = read.document('404')
52        attrs.setdefault('VARS', []).append('userguide_page md `{0}`'.format(name))
53        self.parse_text(doc, attrs)
54
55    def parse_text(self, text, attrs=None):
56        """Parse and translate the document."""
57        self._attrs = attrs or {}
58        t = self._tree = simplemarkdown.Tree()
59        read.Parser().parse(text, t)
60
61    def is_popup(self):
62        """Return True if the helppage should be displayed as a popup."""
63        try:
64            return 'popup' in self._attrs['PROPERTIES']
65        except KeyError:
66            return False
67
68    def title(self):
69        """Return the title"""
70        if self._title is None:
71            self._title = "No Title"
72            for heading in self._tree.find('heading'):
73                self._title = self._tree.text(heading)
74                break
75        return self._title
76
77    def body(self):
78        """Return the HTML body."""
79        if self._body is None:
80            output = HtmlOutput()
81            output.resolver = Resolver(self._attrs.get('VARS'))
82            self._tree.copy(output)
83            html = output.html()
84            # remove empty paragraphs (could result from optional text)
85            html = html.replace('<p></p>', '')
86            self._body = html
87        return self._body
88
89    def children(self):
90        """Return the list of names of child documents."""
91        return self._attrs.get("SUBDOCS") or []
92
93    def seealso(self):
94        """Return the list of names of "see also" documents."""
95        return self._attrs.get("SEEALSO") or []
96
97
98class HtmlOutput(simplemarkdown.HtmlOutput):
99    """Colorizes LilyPond source and replaces {variables}.
100
101    Put a Resolver instance in the resolver attribute before populating
102    the output.
103
104    """
105    heading_offset = 1
106
107    def code_start(self, code, specifier=None):
108        if specifier == "lilypond":
109            import highlight2html
110            self._html.append(highlight2html.html_text(code, full_html=False))
111        else:
112            self.tag('code')
113            self.tag('pre')
114            self.text(code)
115
116    def code_end(self, code, specifier=None):
117        if specifier != "lilypond":
118            self.tag('/pre')
119            self.tag('/code')
120        self.nl()
121
122    def inline_text_start(self, text):
123        text = self.html_escape(text)
124        text = self.resolver.format(text)   # replace {variables} ...
125        self._html.append(text)
126
127
128class Resolver(object):
129    """Resolves variables in help documents."""
130    def __init__(self, variables=None):
131        """Initialize with a list of variables from the #VARS section.
132
133        Every item is simply a line, where the first word is the name,
134        the second the type and the rest is the contents.
135
136        """
137        self._variables = d = {}
138        if variables:
139            for v in variables:
140                try:
141                    name, type, text = v.split(None, 2)
142                except ValueError:
143                    continue
144                d[name] = (type, text)
145
146    def format(self, text):
147        """Replaces all {variable} items in the text."""
148        return read._variable_re.sub(self.replace, text)
149
150    def replace(self, matchObj):
151        """Return the replace string for the match.
152
153        For a match like {blabla}, self.resolve('blabla') is called, and if
154        the result is not None, '{blabla}' is replaced with the result.
155
156        """
157        result = self.resolve(matchObj.group(1))
158        return matchObj.group() if result is None else result
159
160    def resolve(self, name):
161        """Try to find the value for the named variable.
162
163        First, the #VARS section is searched. If that yields no result,
164        the named function in the resolve module is called. If that yields
165        no result either, None is returned.
166
167        """
168
169        try:
170            typ, text = self._variables[name]
171        except KeyError:
172            try:
173                return getattr(resolve, name)()
174            except AttributeError:
175                return
176        try:
177            method = getattr(self, 'handle_' + typ.lower())
178        except AttributeError:
179            method = self.handle_text
180        return method(text)
181
182    def handle_md(self, text):
183        """Convert inline markdown to HTML."""
184        return simplemarkdown.html_inline(text)
185
186    def handle_html(self, text):
187        """Return text as is, it may contain HTML."""
188        return text
189
190    def handle_text(self, text):
191        """Return text escaped, it will not be represented as HTML."""
192        return simplemarkdown.html_escape(text)
193
194    def handle_url(self, text):
195        """Return a clickable url."""
196        url = text
197        if text.startswith('http://'):
198            text = text[7:]
199        if text.endswith('/'):
200            text = text[:-1]
201        url = simplemarkdown.html_escape(url).replace('"', '&quot;')
202        text = simplemarkdown.html_escape(text)
203        return '<a href="{0}">{1}</a>'.format(url, text)
204
205    def handle_help(self, text):
206        """Return a link to the specified help page, with the title."""
207        title = Page(text).title()
208        url = text
209        return '<a href="{0}">{1}</a>'.format(url, title)
210
211    def handle_shortcut(self, text):
212        """Return the keystroke currently defined for the action."""
213        collection_name, action_name = text.split(None, 1)
214        import actioncollectionmanager
215        action = actioncollectionmanager.action(collection_name, action_name)
216        seq = action.shortcut()
217        key = seq.toString(QKeySequence.NativeText) or _("(no key defined)")
218        return '<span class="shortcut">{0}</span>'.format(simplemarkdown.html_escape(key))
219
220    def handle_menu(self, text):
221        """Split the text on '->' in menu or action titles and translate them.
222
223        The pieces are then formatted as a nice menu path.
224        When an item contains a "|", the part before the "|" is the message
225        context.
226
227        When an item starts with "!", the accelerators are not removed (i.e.
228        it is not an action or menu name).
229
230        """
231        pieces = [name.strip() for name in text.split('->')]
232        import qutil
233        def title(name):
234            """Return a translated title for the name."""
235            try:
236                name = {
237                    # untranslated standard menu names
238                    'file': 'menu title|&File',
239                    'edit': 'menu title|&Edit',
240                    'view': 'menu title|&View',
241                    'snippets': 'menu title|Sn&ippets',
242                    'music': 'menu title|&Music',
243                    'lilypond': 'menu title|&LilyPond',
244                    'tools': 'menu title|&Tools',
245                    'window': 'menu title|&Window',
246                    'session': 'menu title|&Session',
247                    'help': 'menu title|&Help',
248                }[name]
249            except KeyError:
250                pass
251            if name.startswith('!'):
252                removeAccel = False
253                name = name[1:]
254            else:
255                removeAccel = True
256            try:
257                ctxt, msg = name.split('|', 1)
258                translation = _(ctxt, msg)
259            except ValueError:
260                translation = _(name)
261            if removeAccel:
262                translation = qutil.removeAccelerator(translation).strip('.')
263            return translation
264
265        translated = [title(name) for name in pieces]
266        return '<em>{0}</em>'.format(' &#8594; '.join(translated))
267
268    def handle_image(self, filename):
269        url = simplemarkdown.html_escape(filename).replace('"', '&quot;')
270        return '<img src="{0}" alt="{0}"/>'.format(url)
271
272    def handle_languagename(self, code):
273        """Return a language name in the current language."""
274        import i18n.setup
275        import language_names
276        return language_names.languageName(code, i18n.setup.current())
277
278