1""" help.py: Implement the Idle help menu. 2Contents are subject to revision at any time, without notice. 3 4 5Help => About IDLE: diplay 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 # used so we exclude page navigation 57 self.hdrlink = False # used so we don't show header links 58 self.level = 0 # indentation level 59 self.pre = False # displaying preformatted text 60 self.hprefix = '' # prefix such as '25.5' to strip from headings 61 self.nested_dl = False # if we're in a nested <dl> 62 self.simplelist = False # 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 66 def indent(self, amt=1): 67 self.level += amt 68 self.tags = '' if self.level == 0 else 'l'+str(self.level) 69 70 def handle_starttag(self, tag, attrs): 71 "Handle starttags in help.html." 72 class_ = '' 73 for a, v in attrs: 74 if a == 'class': 75 class_ = v 76 s = '' 77 if tag == 'div' and class_ == 'section': 78 self.show = True # start of main content 79 elif tag == 'div' and class_ == 'sphinxsidebar': 80 self.show = False # end of main content 81 elif tag == 'p' and class_ != 'first': 82 s = '\n\n' 83 elif tag == 'span' and class_ == 'pre': 84 self.chartags = 'pre' 85 elif tag == 'span' and class_ == 'versionmodified': 86 self.chartags = 'em' 87 elif tag == 'em': 88 self.chartags = 'em' 89 elif tag in ['ul', 'ol']: 90 if class_.find('simple') != -1: 91 s = '\n' 92 self.simplelist = True 93 else: 94 self.simplelist = False 95 self.indent() 96 elif tag == 'dl': 97 if self.level > 0: 98 self.nested_dl = True 99 elif tag == 'li': 100 s = '\n* ' if self.simplelist else '\n\n* ' 101 elif tag == 'dt': 102 s = '\n\n' if not self.nested_dl else '\n' # avoid extra line 103 self.nested_dl = False 104 elif tag == 'dd': 105 self.indent() 106 s = '\n' 107 elif tag == 'pre': 108 self.pre = True 109 if self.show: 110 self.text.insert('end', '\n\n') 111 self.tags = 'preblock' 112 elif tag == 'a' and class_ == 'headerlink': 113 self.hdrlink = True 114 elif tag == 'h1': 115 self.tags = tag 116 elif tag in ['h2', 'h3']: 117 if self.show: 118 self.header = '' 119 self.text.insert('end', '\n\n') 120 self.tags = tag 121 if self.show: 122 self.text.insert('end', s, (self.tags, self.chartags)) 123 124 def handle_endtag(self, tag): 125 "Handle endtags in help.html." 126 if tag in ['h1', 'h2', 'h3']: 127 self.indent(0) # clear tag, reset indent 128 if self.show: 129 self.toc.append((self.header, self.text.index('insert'))) 130 elif tag in ['span', 'em']: 131 self.chartags = '' 132 elif tag == 'a': 133 self.hdrlink = False 134 elif tag == 'pre': 135 self.pre = False 136 self.tags = '' 137 elif tag in ['ul', 'dd', 'ol']: 138 self.indent(amt=-1) 139 140 def handle_data(self, data): 141 "Handle date segments in help.html." 142 if self.show and not self.hdrlink: 143 d = data if self.pre else data.replace('\n', ' ') 144 if self.tags == 'h1': 145 self.hprefix = d[0:d.index(' ')] 146 if self.tags in ['h1', 'h2', 'h3'] and self.hprefix != '': 147 if d[0:len(self.hprefix)] == self.hprefix: 148 d = d[len(self.hprefix):].strip() 149 self.header += d 150 self.text.insert('end', d, (self.tags, self.chartags)) 151 152 153class HelpText(Text): 154 "Display help.html." 155 def __init__(self, parent, filename): 156 "Configure tags and feed file to parser." 157 uwide = idleConf.GetOption('main', 'EditorWindow', 'width', type='int') 158 uhigh = idleConf.GetOption('main', 'EditorWindow', 'height', type='int') 159 uhigh = 3 * uhigh // 4 # lines average 4/3 of editor line height 160 Text.__init__(self, parent, wrap='word', highlightthickness=0, 161 padx=5, borderwidth=0, width=uwide, height=uhigh) 162 163 normalfont = self.findfont(['TkDefaultFont', 'arial', 'helvetica']) 164 fixedfont = self.findfont(['TkFixedFont', 'monaco', 'courier']) 165 self['font'] = (normalfont, 12) 166 self.tag_configure('em', font=(normalfont, 12, 'italic')) 167 self.tag_configure('h1', font=(normalfont, 20, 'bold')) 168 self.tag_configure('h2', font=(normalfont, 18, 'bold')) 169 self.tag_configure('h3', font=(normalfont, 15, 'bold')) 170 self.tag_configure('pre', font=(fixedfont, 12), background='#f6f6ff') 171 self.tag_configure('preblock', font=(fixedfont, 10), lmargin1=25, 172 borderwidth=1, relief='solid', background='#eeffcc') 173 self.tag_configure('l1', lmargin1=25, lmargin2=25) 174 self.tag_configure('l2', lmargin1=50, lmargin2=50) 175 self.tag_configure('l3', lmargin1=75, lmargin2=75) 176 self.tag_configure('l4', lmargin1=100, lmargin2=100) 177 178 self.parser = HelpParser(self) 179 with open(filename, encoding='utf-8') as f: 180 contents = f.read() 181 self.parser.feed(contents) 182 self['state'] = 'disabled' 183 184 def findfont(self, names): 185 "Return name of first font family derived from names." 186 for name in names: 187 if name.lower() in (x.lower() for x in tkfont.names(root=self)): 188 font = tkfont.Font(name=name, exists=True, root=self) 189 return font.actual()['family'] 190 elif name.lower() in (x.lower() 191 for x in tkfont.families(root=self)): 192 return name 193 194 195class HelpFrame(Frame): 196 "Display html text, scrollbar, and toc." 197 def __init__(self, parent, filename): 198 Frame.__init__(self, parent) 199 # keep references to widgets for test access. 200 self.text = text = HelpText(self, filename) 201 self['background'] = text['background'] 202 self.toc = toc = self.toc_menu(text) 203 self.scroll = scroll = Scrollbar(self, command=text.yview) 204 text['yscrollcommand'] = scroll.set 205 206 self.rowconfigure(0, weight=1) 207 self.columnconfigure(1, weight=1) # text 208 toc.grid(row=0, column=0, sticky='nw') 209 text.grid(row=0, column=1, sticky='nsew') 210 scroll.grid(row=0, column=2, sticky='ns') 211 212 def toc_menu(self, text): 213 "Create table of contents as drop-down menu." 214 toc = Menubutton(self, text='TOC') 215 drop = Menu(toc, tearoff=False) 216 for lbl, dex in text.parser.toc: 217 drop.add_command(label=lbl, command=lambda dex=dex:text.yview(dex)) 218 toc['menu'] = drop 219 return toc 220 221 222class HelpWindow(Toplevel): 223 "Display frame with rendered html." 224 def __init__(self, parent, filename, title): 225 Toplevel.__init__(self, parent) 226 self.wm_title(title) 227 self.protocol("WM_DELETE_WINDOW", self.destroy) 228 HelpFrame(self, filename).grid(column=0, row=0, sticky='nsew') 229 self.grid_columnconfigure(0, weight=1) 230 self.grid_rowconfigure(0, weight=1) 231 232 233def copy_strip(): 234 """Copy idle.html to idlelib/help.html, stripping trailing whitespace. 235 236 Files with trailing whitespace cannot be pushed to the hg cpython 237 repository. For 3.x (on Windows), help.html is generated, after 238 editing idle.rst in the earliest maintenance version, with 239 sphinx-build -bhtml . build/html 240 python_d.exe -c "from idlelib.help import copy_strip; copy_strip()" 241 After refreshing TortoiseHG workshop to generate a diff, 242 check both the diff and displayed text. Push the diff along with 243 the idle.rst change and merge both into default (or an intermediate 244 maintenance version). 245 246 When the 'earlist' version gets its final maintenance release, 247 do an update as described above, without editing idle.rst, to 248 rebase help.html on the next version of idle.rst. Do not worry 249 about version changes as version is not displayed. Examine other 250 changes and the result of Help -> IDLE Help. 251 252 If maintenance and default versions of idle.rst diverge, and 253 merging does not go smoothly, then consider generating 254 separate help.html files from separate idle.htmls. 255 """ 256 src = join(abspath(dirname(dirname(dirname(__file__)))), 257 'Doc', 'build', 'html', 'library', 'idle.html') 258 dst = join(abspath(dirname(__file__)), 'help.html') 259 with open(src, 'rb') as inn,\ 260 open(dst, 'wb') as out: 261 for line in inn: 262 out.write(line.rstrip() + b'\n') 263 print('idle.html copied to help.html') 264 265def show_idlehelp(parent): 266 "Create HelpWindow; called from Idle Help event handler." 267 filename = join(abspath(dirname(__file__)), 'help.html') 268 if not isfile(filename): 269 # try copy_strip, present message 270 return 271 HelpWindow(parent, filename, 'IDLE Help (%s)' % python_version()) 272 273if __name__ == '__main__': 274 from idlelib.idle_test.htest import run 275 run(show_idlehelp) 276