1""" help.py: Implement the Idle help menu. 2Contents are subject to revision at any time, without notice. 3 4 5Help => About IDLE: display About Idle dialog 6 7<to be moved here from help_about.py> 8 9 10Help => IDLE Help: Display help.html with proper formatting. 11Doc/library/idle.rst (Sphinx)=> Doc/build/html/library/idle.html 12(help.copy_strip)=> Lib/idlelib/help.html 13 14HelpParser - Parse help.html and render to tk Text. 15 16HelpText - Display formatted help.html. 17 18HelpFrame - Contain text, scrollbar, and table-of-contents. 19(This will be needed for display in a future tabbed window.) 20 21HelpWindow - Display HelpFrame in a standalone window. 22 23copy_strip - Copy idle.html to help.html, rstripping each line. 24 25show_idlehelp - Create HelpWindow. Called in EditorWindow.help_dialog. 26""" 27from html.parser import HTMLParser 28from os.path import abspath, dirname, isfile, join 29from platform import python_version 30 31from tkinter import Toplevel, Frame, Text, Menu 32from tkinter.ttk import Menubutton, Scrollbar 33from tkinter import font as tkfont 34 35from idlelib.config import idleConf 36 37## About IDLE ## 38 39 40## IDLE Help ## 41 42class HelpParser(HTMLParser): 43 """Render help.html into a text widget. 44 45 The overridden handle_xyz methods handle a subset of html tags. 46 The supplied text should have the needed tag configurations. 47 The behavior for unsupported tags, such as table, is undefined. 48 If the tags generated by Sphinx change, this class, especially 49 the handle_starttag and handle_endtags methods, might have to also. 50 """ 51 def __init__(self, text): 52 HTMLParser.__init__(self, convert_charrefs=True) 53 self.text = text # Text widget we're rendering into. 54 self.tags = '' # Current block level text tags to apply. 55 self.chartags = '' # Current character level text tags. 56 self.show = False # Exclude html page navigation. 57 self.hdrlink = False # Exclude html header links. 58 self.level = 0 # Track indentation level. 59 self.pre = False # Displaying preformatted text? 60 self.hprefix = '' # Heading prefix (like '25.5'?) to remove. 61 self.nested_dl = False # In a nested <dl>? 62 self.simplelist = False # In a simple list (no double spacing)? 63 self.toc = [] # Pair headers with text indexes for toc. 64 self.header = '' # Text within header tags for toc. 65 self.prevtag = None # Previous tag info (opener?, tag). 66 67 def indent(self, amt=1): 68 "Change indent (+1, 0, -1) and tags." 69 self.level += amt 70 self.tags = '' if self.level == 0 else 'l'+str(self.level) 71 72 def handle_starttag(self, tag, attrs): 73 "Handle starttags in help.html." 74 class_ = '' 75 for a, v in attrs: 76 if a == 'class': 77 class_ = v 78 s = '' 79 if tag == 'div' and class_ == 'section': 80 self.show = True # Start main content. 81 elif tag == 'div' and class_ == 'sphinxsidebar': 82 self.show = False # End main content. 83 elif tag == 'p' and self.prevtag and not self.prevtag[0]: 84 # Begin a new block for <p> tags after a closed tag. 85 # Avoid extra lines, e.g. after <pre> tags. 86 lastline = self.text.get('end-1c linestart', 'end-1c') 87 s = '\n\n' if lastline and not lastline.isspace() else '\n' 88 elif tag == 'span' and class_ == 'pre': 89 self.chartags = 'pre' 90 elif tag == 'span' and class_ == 'versionmodified': 91 self.chartags = 'em' 92 elif tag == 'em': 93 self.chartags = 'em' 94 elif tag in ['ul', 'ol']: 95 if class_.find('simple') != -1: 96 s = '\n' 97 self.simplelist = True 98 else: 99 self.simplelist = False 100 self.indent() 101 elif tag == 'dl': 102 if self.level > 0: 103 self.nested_dl = True 104 elif tag == 'li': 105 s = '\n* ' if self.simplelist else '\n\n* ' 106 elif tag == 'dt': 107 s = '\n\n' if not self.nested_dl else '\n' # Avoid extra line. 108 self.nested_dl = False 109 elif tag == 'dd': 110 self.indent() 111 s = '\n' 112 elif tag == 'pre': 113 self.pre = True 114 if self.show: 115 self.text.insert('end', '\n\n') 116 self.tags = 'preblock' 117 elif tag == 'a' and class_ == 'headerlink': 118 self.hdrlink = True 119 elif tag == 'h1': 120 self.tags = tag 121 elif tag in ['h2', 'h3']: 122 if self.show: 123 self.header = '' 124 self.text.insert('end', '\n\n') 125 self.tags = tag 126 if self.show: 127 self.text.insert('end', s, (self.tags, self.chartags)) 128 self.prevtag = (True, tag) 129 130 def handle_endtag(self, tag): 131 "Handle endtags in help.html." 132 if tag in ['h1', 'h2', 'h3']: 133 assert self.level == 0 134 if self.show: 135 indent = (' ' if tag == 'h3' else 136 ' ' if tag == 'h2' else 137 '') 138 self.toc.append((indent+self.header, self.text.index('insert'))) 139 self.tags = '' 140 elif tag in ['span', 'em']: 141 self.chartags = '' 142 elif tag == 'a': 143 self.hdrlink = False 144 elif tag == 'pre': 145 self.pre = False 146 self.tags = '' 147 elif tag in ['ul', 'dd', 'ol']: 148 self.indent(-1) 149 self.prevtag = (False, tag) 150 151 def handle_data(self, data): 152 "Handle date segments in help.html." 153 if self.show and not self.hdrlink: 154 d = data if self.pre else data.replace('\n', ' ') 155 if self.tags == 'h1': 156 try: 157 self.hprefix = d[0:d.index(' ')] 158 except ValueError: 159 self.hprefix = '' 160 if self.tags in ['h1', 'h2', 'h3']: 161 if (self.hprefix != '' and 162 d[0:len(self.hprefix)] == self.hprefix): 163 d = d[len(self.hprefix):] 164 self.header += d.strip() 165 self.text.insert('end', d, (self.tags, self.chartags)) 166 167 168class HelpText(Text): 169 "Display help.html." 170 def __init__(self, parent, filename): 171 "Configure tags and feed file to parser." 172 uwide = idleConf.GetOption('main', 'EditorWindow', 'width', type='int') 173 uhigh = idleConf.GetOption('main', 'EditorWindow', 'height', type='int') 174 uhigh = 3 * uhigh // 4 # Lines average 4/3 of editor line height. 175 Text.__init__(self, parent, wrap='word', highlightthickness=0, 176 padx=5, borderwidth=0, width=uwide, height=uhigh) 177 178 normalfont = self.findfont(['TkDefaultFont', 'arial', 'helvetica']) 179 fixedfont = self.findfont(['TkFixedFont', 'monaco', 'courier']) 180 self['font'] = (normalfont, 12) 181 self.tag_configure('em', font=(normalfont, 12, 'italic')) 182 self.tag_configure('h1', font=(normalfont, 20, 'bold')) 183 self.tag_configure('h2', font=(normalfont, 18, 'bold')) 184 self.tag_configure('h3', font=(normalfont, 15, 'bold')) 185 self.tag_configure('pre', font=(fixedfont, 12), background='#f6f6ff') 186 self.tag_configure('preblock', font=(fixedfont, 10), lmargin1=25, 187 borderwidth=1, relief='solid', background='#eeffcc') 188 self.tag_configure('l1', lmargin1=25, lmargin2=25) 189 self.tag_configure('l2', lmargin1=50, lmargin2=50) 190 self.tag_configure('l3', lmargin1=75, lmargin2=75) 191 self.tag_configure('l4', lmargin1=100, lmargin2=100) 192 193 self.parser = HelpParser(self) 194 with open(filename, encoding='utf-8') as f: 195 contents = f.read() 196 self.parser.feed(contents) 197 self['state'] = 'disabled' 198 199 def findfont(self, names): 200 "Return name of first font family derived from names." 201 for name in names: 202 if name.lower() in (x.lower() for x in tkfont.names(root=self)): 203 font = tkfont.Font(name=name, exists=True, root=self) 204 return font.actual()['family'] 205 elif name.lower() in (x.lower() 206 for x in tkfont.families(root=self)): 207 return name 208 209 210class HelpFrame(Frame): 211 "Display html text, scrollbar, and toc." 212 def __init__(self, parent, filename): 213 Frame.__init__(self, parent) 214 self.text = text = HelpText(self, filename) 215 self['background'] = text['background'] 216 self.toc = toc = self.toc_menu(text) 217 self.scroll = scroll = Scrollbar(self, command=text.yview) 218 text['yscrollcommand'] = scroll.set 219 220 self.rowconfigure(0, weight=1) 221 self.columnconfigure(1, weight=1) # Only expand the text widget. 222 toc.grid(row=0, column=0, sticky='nw') 223 text.grid(row=0, column=1, sticky='nsew') 224 scroll.grid(row=0, column=2, sticky='ns') 225 226 def toc_menu(self, text): 227 "Create table of contents as drop-down menu." 228 toc = Menubutton(self, text='TOC') 229 drop = Menu(toc, tearoff=False) 230 for lbl, dex in text.parser.toc: 231 drop.add_command(label=lbl, command=lambda dex=dex:text.yview(dex)) 232 toc['menu'] = drop 233 return toc 234 235 236class HelpWindow(Toplevel): 237 "Display frame with rendered html." 238 def __init__(self, parent, filename, title): 239 Toplevel.__init__(self, parent) 240 self.wm_title(title) 241 self.protocol("WM_DELETE_WINDOW", self.destroy) 242 HelpFrame(self, filename).grid(column=0, row=0, sticky='nsew') 243 self.grid_columnconfigure(0, weight=1) 244 self.grid_rowconfigure(0, weight=1) 245 246 247def copy_strip(): 248 """Copy idle.html to idlelib/help.html, stripping trailing whitespace. 249 250 Files with trailing whitespace cannot be pushed to the git cpython 251 repository. For 3.x (on Windows), help.html is generated, after 252 editing idle.rst on the master branch, with 253 sphinx-build -bhtml . build/html 254 python_d.exe -c "from idlelib.help import copy_strip; copy_strip()" 255 Check build/html/library/idle.html, the help.html diff, and the text 256 displayed by Help => IDLE Help. Add a blurb and create a PR. 257 258 It can be worthwhile to occasionally generate help.html without 259 touching idle.rst. Changes to the master version and to the doc 260 build system may result in changes that should not changed 261 the displayed text, but might break HelpParser. 262 263 As long as master and maintenance versions of idle.rst remain the 264 same, help.html can be backported. The internal Python version 265 number is not displayed. If maintenance idle.rst diverges from 266 the master version, then instead of backporting help.html from 267 master, repeat the procedure above to generate a maintenance 268 version. 269 """ 270 src = join(abspath(dirname(dirname(dirname(__file__)))), 271 'Doc', 'build', 'html', 'library', 'idle.html') 272 dst = join(abspath(dirname(__file__)), 'help.html') 273 with open(src, 'rb') as inn,\ 274 open(dst, 'wb') as out: 275 for line in inn: 276 out.write(line.rstrip() + b'\n') 277 print(f'{src} copied to {dst}') 278 279def show_idlehelp(parent): 280 "Create HelpWindow; called from Idle Help event handler." 281 filename = join(abspath(dirname(__file__)), 'help.html') 282 if not isfile(filename): 283 # Try copy_strip, present message. 284 return 285 HelpWindow(parent, filename, 'IDLE Help (%s)' % python_version()) 286 287if __name__ == '__main__': 288 from unittest import main 289 main('idlelib.idle_test.test_help', verbosity=2, exit=False) 290 291 from idlelib.idle_test.htest import run 292 run(show_idlehelp) 293