help.py 9.47 KB
Newer Older
1 2
""" help.py: Implement the Idle help menu.
Contents are subject to revision at any time, without notice.
3 4 5 6 7 8 9


Help => About IDLE: diplay About Idle dialog

<to be moved here from aboutDialog.py>


10 11 12 13 14
Help => IDLE Help: Display help.html with proper formatting.
Doc/library/idle.rst (Sphinx)=> Doc/build/html/library/idle.html
(help.copy_strip)=> Lib/idlelib/help.html

HelpParser - Parse help.html and and render to tk Text.
15

16
HelpText - Display formatted help.html.
17

18
HelpFrame - Contain text, scrollbar, and table-of-contents.
19 20
(This will be needed for display in a future tabbed window.)

21 22 23
HelpWindow - Display HelpFrame in a standalone window.

copy_strip - Copy idle.html to help.html, rstripping each line.
24 25 26 27 28 29 30

show_idlehelp - Create HelpWindow.  Called in EditorWindow.help_dialog.
"""
from html.parser import HTMLParser
from os.path import abspath, dirname, isdir, isfile, join
from tkinter import Tk, Toplevel, Frame, Text, Scrollbar, Menu, Menubutton
from tkinter import font as tkfont
31
from idlelib.configHandler import idleConf
32 33 34 35 36 37 38 39 40 41 42

use_ttk = False # until available to import
if use_ttk:
    from tkinter.ttk import Menubutton

## About IDLE ##


## IDLE Help ##

class HelpParser(HTMLParser):
43
    """Render help.html into a text widget.
44 45 46 47 48 49 50 51

    The overridden handle_xyz methods handle a subset of html tags.
    The supplied text should have the needed tag configurations.
    The behavior for unsupported tags, such as table, is undefined.
    """
    def __init__(self, text):
        HTMLParser.__init__(self, convert_charrefs=True)
        self.text = text         # text widget we're rendering into
52 53
        self.tags = ''           # current block level text tags to apply
        self.chartags = ''       # current character level text tags
54 55 56 57
        self.show = False        # used so we exclude page navigation
        self.hdrlink = False     # used so we don't show header links
        self.level = 0           # indentation level
        self.pre = False         # displaying preformatted text
58
        self.hprefix = ''        # prefix such as '25.5' to strip from headings
59 60
        self.nested_dl = False   # if we're in a nested <dl>
        self.simplelist = False  # simple list (no double spacing)
61 62
        self.toc = []            # pair headers with text indexes for toc
        self.header = ''         # text within header tags for toc
63 64 65 66 67 68

    def indent(self, amt=1):
        self.level += amt
        self.tags = '' if self.level == 0 else 'l'+str(self.level)

    def handle_starttag(self, tag, attrs):
69
        "Handle starttags in help.html."
70 71 72 73 74 75 76 77 78 79 80 81
        class_ = ''
        for a, v in attrs:
            if a == 'class':
                class_ = v
        s = ''
        if tag == 'div' and class_ == 'section':
            self.show = True    # start of main content
        elif tag == 'div' and class_ == 'sphinxsidebar':
            self.show = False   # end of main content
        elif tag == 'p' and class_ != 'first':
            s = '\n\n'
        elif tag == 'span' and class_ == 'pre':
82
            self.chartags = 'pre'
83
        elif tag == 'span' and class_ == 'versionmodified':
84
            self.chartags = 'em'
85
        elif tag == 'em':
86
            self.chartags = 'em'
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
        elif tag in ['ul', 'ol']:
            if class_.find('simple') != -1:
                s = '\n'
                self.simplelist = True
            else:
                self.simplelist = False
            self.indent()
        elif tag == 'dl':
            if self.level > 0:
                self.nested_dl = True
        elif tag == 'li':
            s = '\n* ' if self.simplelist else '\n\n* '
        elif tag == 'dt':
            s = '\n\n' if not self.nested_dl else '\n'  # avoid extra line
            self.nested_dl = False
        elif tag == 'dd':
            self.indent()
            s = '\n'
        elif tag == 'pre':
            self.pre = True
            if self.show:
                self.text.insert('end', '\n\n')
            self.tags = 'preblock'
        elif tag == 'a' and class_ == 'headerlink':
            self.hdrlink = True
        elif tag == 'h1':
            self.tags = tag
        elif tag in ['h2', 'h3']:
            if self.show:
116
                self.header = ''
117 118 119
                self.text.insert('end', '\n\n')
            self.tags = tag
        if self.show:
120
            self.text.insert('end', s, (self.tags, self.chartags))
121 122

    def handle_endtag(self, tag):
123
        "Handle endtags in help.html."
124
        if tag in ['h1', 'h2', 'h3']:
125
            self.indent(0)  # clear tag, reset indent
126 127
            if self.show:
                self.toc.append((self.header, self.text.index('insert')))
128 129
        elif tag in ['span', 'em']:
            self.chartags = ''
130 131 132 133 134 135 136 137 138
        elif tag == 'a':
            self.hdrlink = False
        elif tag == 'pre':
            self.pre = False
            self.tags = ''
        elif tag in ['ul', 'dd', 'ol']:
            self.indent(amt=-1)

    def handle_data(self, data):
139
        "Handle date segments in help.html."
140 141 142 143 144 145 146
        if self.show and not self.hdrlink:
            d = data if self.pre else data.replace('\n', ' ')
            if self.tags == 'h1':
                self.hprefix = d[0:d.index(' ')]
            if self.tags in ['h1', 'h2', 'h3'] and self.hprefix != '':
                if d[0:len(self.hprefix)] == self.hprefix:
                    d = d[len(self.hprefix):].strip()
147
                self.header += d
148
            self.text.insert('end', d, (self.tags, self.chartags))
149 150 151


class HelpText(Text):
152
    "Display help.html."
153 154
    def __init__(self, parent, filename):
        "Configure tags and feed file to parser."
155 156 157
        uwide = idleConf.GetOption('main', 'EditorWindow', 'width', type='int')
        uhigh = idleConf.GetOption('main', 'EditorWindow', 'height', type='int')
        uhigh = 3 * uhigh // 4  # lines average 4/3 of editor line height
158
        Text.__init__(self, parent, wrap='word', highlightthickness=0,
159
                      padx=5, borderwidth=0, width=uwide, height=uhigh)
160 161 162 163 164 165 166 167

        normalfont = self.findfont(['TkDefaultFont', 'arial', 'helvetica'])
        fixedfont = self.findfont(['TkFixedFont', 'monaco', 'courier'])
        self['font'] = (normalfont, 12)
        self.tag_configure('em', font=(normalfont, 12, 'italic'))
        self.tag_configure('h1', font=(normalfont, 20, 'bold'))
        self.tag_configure('h2', font=(normalfont, 18, 'bold'))
        self.tag_configure('h3', font=(normalfont, 15, 'bold'))
168
        self.tag_configure('pre', font=(fixedfont, 12), background='#f6f6ff')
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193
        self.tag_configure('preblock', font=(fixedfont, 10), lmargin1=25,
                borderwidth=1, relief='solid', background='#eeffcc')
        self.tag_configure('l1', lmargin1=25, lmargin2=25)
        self.tag_configure('l2', lmargin1=50, lmargin2=50)
        self.tag_configure('l3', lmargin1=75, lmargin2=75)
        self.tag_configure('l4', lmargin1=100, lmargin2=100)

        self.parser = HelpParser(self)
        with open(filename, encoding='utf-8') as f:
            contents = f.read()
        self.parser.feed(contents)
        self['state'] = 'disabled'

    def findfont(self, names):
        "Return name of first font family derived from names."
        for name in names:
            if name.lower() in (x.lower() for x in tkfont.names(root=self)):
                font = tkfont.Font(name=name, exists=True, root=self)
                return font.actual()['family']
            elif name.lower() in (x.lower()
                                  for x in tkfont.families(root=self)):
                return name


class HelpFrame(Frame):
194
    "Display html text, scrollbar, and toc."
195 196 197 198 199 200
    def __init__(self, parent, filename):
        Frame.__init__(self, parent)
        text = HelpText(self, filename)
        self['background'] = text['background']
        scroll = Scrollbar(self, command=text.yview)
        text['yscrollcommand'] = scroll.set
201 202 203
        self.rowconfigure(0, weight=1)
        self.columnconfigure(1, weight=1)  # text
        self.toc_menu(text).grid(column=0, row=0, sticky='nw')
204 205 206
        text.grid(column=1, row=0, sticky='nsew')
        scroll.grid(column=2, row=0, sticky='ns')

207 208
    def toc_menu(self, text):
        "Create table of contents as drop-down menu."
209 210
        toc = Menubutton(self, text='TOC')
        drop = Menu(toc, tearoff=False)
211 212
        for lbl, dex in text.parser.toc:
            drop.add_command(label=lbl, command=lambda dex=dex:text.yview(dex))
213 214 215 216 217
        toc['menu'] = drop
        return toc


class HelpWindow(Toplevel):
218
    "Display frame with rendered html."
219 220 221 222 223 224 225 226 227
    def __init__(self, parent, filename, title):
        Toplevel.__init__(self, parent)
        self.wm_title(title)
        self.protocol("WM_DELETE_WINDOW", self.destroy)
        HelpFrame(self, filename).grid(column=0, row=0, sticky='nsew')
        self.grid_columnconfigure(0, weight=1)
        self.grid_rowconfigure(0, weight=1)


228 229 230 231 232 233 234 235
def copy_strip():
    "Copy idle.html to idlelib/help.html, stripping trailing whitespace."
    src = join(abspath(dirname(dirname(dirname(__file__)))),
               'Doc', 'build', 'html', 'library', 'idle.html')
    dst = join(abspath(dirname(__file__)), 'help.html')
    with open(src, 'rb') as inn,\
         open(dst, 'wb') as out:
        for line in inn:
236
            out.write(line.rstrip() + b'\n')
237 238
    print('idle.html copied to help.html')

239
def show_idlehelp(parent):
240 241
    "Create HelpWindow; called from Idle Help event handler."
    filename = join(abspath(dirname(__file__)), 'help.html')
242
    if not isfile(filename):
243
        # try copy_strip, present message
Terry Jan Reedy's avatar
Terry Jan Reedy committed
244
        return
245 246 247 248 249
    HelpWindow(parent, filename, 'IDLE Help')

if __name__ == '__main__':
    from idlelib.idle_test.htest import run
    run(show_idlehelp)