(root)/
Python-3.12.0/
Lib/
idlelib/
help.py
       1  """ help.py: Implement the Idle help menu.
       2  Contents are subject to revision at any time, without notice.
       3  
       4  
       5  Help => About IDLE: display About Idle dialog
       6  
       7  <to be moved here from help_about.py>
       8  
       9  
      10  Help => IDLE Help: Display help.html with proper formatting.
      11  Doc/library/idle.rst (Sphinx)=> Doc/build/html/library/idle.html
      12  (help.copy_strip)=> Lib/idlelib/help.html
      13  
      14  HelpParser - Parse help.html and render to tk Text.
      15  
      16  HelpText - Display formatted help.html.
      17  
      18  HelpFrame - Contain text, scrollbar, and table-of-contents.
      19  (This will be needed for display in a future tabbed window.)
      20  
      21  HelpWindow - Display HelpFrame in a standalone window.
      22  
      23  copy_strip - Copy idle.html to help.html, rstripping each line.
      24  
      25  show_idlehelp - Create HelpWindow.  Called in EditorWindow.help_dialog.
      26  """
      27  from html.parser import HTMLParser
      28  from os.path import abspath, dirname, isfile, join
      29  from platform import python_version
      30  
      31  from tkinter import Toplevel, Text, Menu
      32  from tkinter.ttk import Frame, Menubutton, Scrollbar, Style
      33  from tkinter import font as tkfont
      34  
      35  from idlelib.config import idleConf
      36  
      37  ## About IDLE ##
      38  
      39  
      40  ## IDLE Help ##
      41  
      42  class ESC[4;38;5;81mHelpParser(ESC[4;38;5;149mHTMLParser):
      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 == 'section' and attrs == [('id', 'idle')]:
      80              self.show = True    # Start main content.
      81          elif tag == 'div' and class_ == 'clearer':
      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  
     168  class ESC[4;38;5;81mHelpText(ESC[4;38;5;149mText):
     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  
     210  class ESC[4;38;5;81mHelpFrame(ESC[4;38;5;149mFrame):
     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  
     238  class ESC[4;38;5;81mHelpWindow(ESC[4;38;5;149mToplevel):
     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  
     249  def 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  
     281  def 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  
     289  if __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)