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, Text, Menu 32from tkinter.ttk import Frame, Menubutton, Scrollbar, Style 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.style = Style(parent) 216 self['style'] = 'helpframe.TFrame' 217 self.style.configure('helpframe.TFrame', background=text['background']) 218 self.toc = toc = self.toc_menu(text) 219 self.scroll = scroll = Scrollbar(self, command=text.yview) 220 text['yscrollcommand'] = scroll.set 221 222 self.rowconfigure(0, weight=1) 223 self.columnconfigure(1, weight=1) # Only expand the text widget. 224 toc.grid(row=0, column=0, sticky='nw') 225 text.grid(row=0, column=1, sticky='nsew') 226 scroll.grid(row=0, column=2, sticky='ns') 227 228 def toc_menu(self, text): 229 "Create table of contents as drop-down menu." 230 toc = Menubutton(self, text='TOC') 231 drop = Menu(toc, tearoff=False) 232 for lbl, dex in text.parser.toc: 233 drop.add_command(label=lbl, command=lambda dex=dex:text.yview(dex)) 234 toc['menu'] = drop 235 return toc 236 237 238class HelpWindow(Toplevel): 239 "Display frame with rendered html." 240 def __init__(self, parent, filename, title): 241 Toplevel.__init__(self, parent) 242 self.wm_title(title) 243 self.protocol("WM_DELETE_WINDOW", self.destroy) 244 HelpFrame(self, filename).grid(column=0, row=0, sticky='nsew') 245 self.grid_columnconfigure(0, weight=1) 246 self.grid_rowconfigure(0, weight=1) 247 248 249def copy_strip(): 250 """Copy idle.html to idlelib/help.html, stripping trailing whitespace. 251 252 Files with trailing whitespace cannot be pushed to the git cpython 253 repository. For 3.x (on Windows), help.html is generated, after 254 editing idle.rst on the master branch, with 255 sphinx-build -bhtml . build/html 256 python_d.exe -c "from idlelib.help import copy_strip; copy_strip()" 257 Check build/html/library/idle.html, the help.html diff, and the text 258 displayed by Help => IDLE Help. Add a blurb and create a PR. 259 260 It can be worthwhile to occasionally generate help.html without 261 touching idle.rst. Changes to the master version and to the doc 262 build system may result in changes that should not changed 263 the displayed text, but might break HelpParser. 264 265 As long as master and maintenance versions of idle.rst remain the 266 same, help.html can be backported. The internal Python version 267 number is not displayed. If maintenance idle.rst diverges from 268 the master version, then instead of backporting help.html from 269 master, repeat the procedure above to generate a maintenance 270 version. 271 """ 272 src = join(abspath(dirname(dirname(dirname(__file__)))), 273 'Doc', 'build', 'html', 'library', 'idle.html') 274 dst = join(abspath(dirname(__file__)), 'help.html') 275 with open(src, 'rb') as inn,\ 276 open(dst, 'wb') as out: 277 for line in inn: 278 out.write(line.rstrip() + b'\n') 279 print(f'{src} copied to {dst}') 280 281def show_idlehelp(parent): 282 "Create HelpWindow; called from Idle Help event handler." 283 filename = join(abspath(dirname(__file__)), 'help.html') 284 if not isfile(filename): 285 # Try copy_strip, present message. 286 return 287 HelpWindow(parent, filename, 'IDLE Help (%s)' % python_version()) 288 289if __name__ == '__main__': 290 from unittest import main 291 main('idlelib.idle_test.test_help', verbosity=2, exit=False) 292 293 from idlelib.idle_test.htest import run 294 run(show_idlehelp) 295