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