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('"', '"') 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(' → '.join(translated)) 267 268 def handle_image(self, filename): 269 url = simplemarkdown.html_escape(filename).replace('"', '"') 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