| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937 |
- # Natural Language Toolkit: Shift-Reduce Parser Application
- #
- # Copyright (C) 2001-2020 NLTK Project
- # Author: Edward Loper <edloper@gmail.com>
- # URL: <http://nltk.org/>
- # For license information, see LICENSE.TXT
- """
- A graphical tool for exploring the shift-reduce parser.
- The shift-reduce parser maintains a stack, which records the structure
- of the portion of the text that has been parsed. The stack is
- initially empty. Its contents are shown on the left side of the main
- canvas.
- On the right side of the main canvas is the remaining text. This is
- the portion of the text which has not yet been considered by the
- parser.
- The parser builds up a tree structure for the text using two
- operations:
- - "shift" moves the first token from the remaining text to the top
- of the stack. In the demo, the top of the stack is its right-hand
- side.
- - "reduce" uses a grammar production to combine the rightmost stack
- elements into a single tree token.
- You can control the parser's operation by using the "shift" and
- "reduce" buttons; or you can use the "step" button to let the parser
- automatically decide which operation to apply. The parser uses the
- following rules to decide which operation to apply:
- - Only shift if no reductions are available.
- - If multiple reductions are available, then apply the reduction
- whose CFG production is listed earliest in the grammar.
- The "reduce" button applies the reduction whose CFG production is
- listed earliest in the grammar. There are two ways to manually choose
- which reduction to apply:
- - Click on a CFG production from the list of available reductions,
- on the left side of the main window. The reduction based on that
- production will be applied to the top of the stack.
- - Click on one of the stack elements. A popup window will appear,
- containing all available reductions. Select one, and it will be
- applied to the top of the stack.
- Note that reductions can only be applied to the top of the stack.
- Keyboard Shortcuts::
- [Space]\t Perform the next shift or reduce operation
- [s]\t Perform a shift operation
- [r]\t Perform a reduction operation
- [Ctrl-z]\t Undo most recent operation
- [Delete]\t Reset the parser
- [g]\t Show/hide available production list
- [Ctrl-a]\t Toggle animations
- [h]\t Help
- [Ctrl-p]\t Print
- [q]\t Quit
- """
- from tkinter.font import Font
- from tkinter import IntVar, Listbox, Button, Frame, Label, Menu, Scrollbar, Tk
- from nltk.tree import Tree
- from nltk.parse import SteppingShiftReduceParser
- from nltk.util import in_idle
- from nltk.draw.util import CanvasFrame, EntryDialog, ShowText, TextWidget
- from nltk.draw import CFGEditor, TreeSegmentWidget, tree_to_treesegment
- """
- Possible future improvements:
- - button/window to change and/or select text. Just pop up a window
- with an entry, and let them modify the text; and then retokenize
- it? Maybe give a warning if it contains tokens whose types are
- not in the grammar.
- - button/window to change and/or select grammar. Select from
- several alternative grammars? Or actually change the grammar? If
- the later, then I'd want to define nltk.draw.cfg, which would be
- responsible for that.
- """
- class ShiftReduceApp(object):
- """
- A graphical tool for exploring the shift-reduce parser. The tool
- displays the parser's stack and the remaining text, and allows the
- user to control the parser's operation. In particular, the user
- can shift tokens onto the stack, and can perform reductions on the
- top elements of the stack. A "step" button simply steps through
- the parsing process, performing the operations that
- ``nltk.parse.ShiftReduceParser`` would use.
- """
- def __init__(self, grammar, sent, trace=0):
- self._sent = sent
- self._parser = SteppingShiftReduceParser(grammar, trace)
- # Set up the main window.
- self._top = Tk()
- self._top.title("Shift Reduce Parser Application")
- # Animations. animating_lock is a lock to prevent the demo
- # from performing new operations while it's animating.
- self._animating_lock = 0
- self._animate = IntVar(self._top)
- self._animate.set(10) # = medium
- # The user can hide the grammar.
- self._show_grammar = IntVar(self._top)
- self._show_grammar.set(1)
- # Initialize fonts.
- self._init_fonts(self._top)
- # Set up key bindings.
- self._init_bindings()
- # Create the basic frames.
- self._init_menubar(self._top)
- self._init_buttons(self._top)
- self._init_feedback(self._top)
- self._init_grammar(self._top)
- self._init_canvas(self._top)
- # A popup menu for reducing.
- self._reduce_menu = Menu(self._canvas, tearoff=0)
- # Reset the demo, and set the feedback frame to empty.
- self.reset()
- self._lastoper1["text"] = ""
- #########################################
- ## Initialization Helpers
- #########################################
- def _init_fonts(self, root):
- # See: <http://www.astro.washington.edu/owen/ROTKFolklore.html>
- self._sysfont = Font(font=Button()["font"])
- root.option_add("*Font", self._sysfont)
- # TWhat's our font size (default=same as sysfont)
- self._size = IntVar(root)
- self._size.set(self._sysfont.cget("size"))
- self._boldfont = Font(family="helvetica", weight="bold", size=self._size.get())
- self._font = Font(family="helvetica", size=self._size.get())
- def _init_grammar(self, parent):
- # Grammar view.
- self._prodframe = listframe = Frame(parent)
- self._prodframe.pack(fill="both", side="left", padx=2)
- self._prodlist_label = Label(
- self._prodframe, font=self._boldfont, text="Available Reductions"
- )
- self._prodlist_label.pack()
- self._prodlist = Listbox(
- self._prodframe,
- selectmode="single",
- relief="groove",
- background="white",
- foreground="#909090",
- font=self._font,
- selectforeground="#004040",
- selectbackground="#c0f0c0",
- )
- self._prodlist.pack(side="right", fill="both", expand=1)
- self._productions = list(self._parser.grammar().productions())
- for production in self._productions:
- self._prodlist.insert("end", (" %s" % production))
- self._prodlist.config(height=min(len(self._productions), 25))
- # Add a scrollbar if there are more than 25 productions.
- if 1: # len(self._productions) > 25:
- listscroll = Scrollbar(self._prodframe, orient="vertical")
- self._prodlist.config(yscrollcommand=listscroll.set)
- listscroll.config(command=self._prodlist.yview)
- listscroll.pack(side="left", fill="y")
- # If they select a production, apply it.
- self._prodlist.bind("<<ListboxSelect>>", self._prodlist_select)
- # When they hover over a production, highlight it.
- self._hover = -1
- self._prodlist.bind("<Motion>", self._highlight_hover)
- self._prodlist.bind("<Leave>", self._clear_hover)
- def _init_bindings(self):
- # Quit
- self._top.bind("<Control-q>", self.destroy)
- self._top.bind("<Control-x>", self.destroy)
- self._top.bind("<Alt-q>", self.destroy)
- self._top.bind("<Alt-x>", self.destroy)
- # Ops (step, shift, reduce, undo)
- self._top.bind("<space>", self.step)
- self._top.bind("<s>", self.shift)
- self._top.bind("<Alt-s>", self.shift)
- self._top.bind("<Control-s>", self.shift)
- self._top.bind("<r>", self.reduce)
- self._top.bind("<Alt-r>", self.reduce)
- self._top.bind("<Control-r>", self.reduce)
- self._top.bind("<Delete>", self.reset)
- self._top.bind("<u>", self.undo)
- self._top.bind("<Alt-u>", self.undo)
- self._top.bind("<Control-u>", self.undo)
- self._top.bind("<Control-z>", self.undo)
- self._top.bind("<BackSpace>", self.undo)
- # Misc
- self._top.bind("<Control-p>", self.postscript)
- self._top.bind("<Control-h>", self.help)
- self._top.bind("<F1>", self.help)
- self._top.bind("<Control-g>", self.edit_grammar)
- self._top.bind("<Control-t>", self.edit_sentence)
- # Animation speed control
- self._top.bind("-", lambda e, a=self._animate: a.set(20))
- self._top.bind("=", lambda e, a=self._animate: a.set(10))
- self._top.bind("+", lambda e, a=self._animate: a.set(4))
- def _init_buttons(self, parent):
- # Set up the frames.
- self._buttonframe = buttonframe = Frame(parent)
- buttonframe.pack(fill="none", side="bottom")
- Button(
- buttonframe,
- text="Step",
- background="#90c0d0",
- foreground="black",
- command=self.step,
- ).pack(side="left")
- Button(
- buttonframe,
- text="Shift",
- underline=0,
- background="#90f090",
- foreground="black",
- command=self.shift,
- ).pack(side="left")
- Button(
- buttonframe,
- text="Reduce",
- underline=0,
- background="#90f090",
- foreground="black",
- command=self.reduce,
- ).pack(side="left")
- Button(
- buttonframe,
- text="Undo",
- underline=0,
- background="#f0a0a0",
- foreground="black",
- command=self.undo,
- ).pack(side="left")
- def _init_menubar(self, parent):
- menubar = Menu(parent)
- filemenu = Menu(menubar, tearoff=0)
- filemenu.add_command(
- label="Reset Parser", underline=0, command=self.reset, accelerator="Del"
- )
- filemenu.add_command(
- label="Print to Postscript",
- underline=0,
- command=self.postscript,
- accelerator="Ctrl-p",
- )
- filemenu.add_command(
- label="Exit", underline=1, command=self.destroy, accelerator="Ctrl-x"
- )
- menubar.add_cascade(label="File", underline=0, menu=filemenu)
- editmenu = Menu(menubar, tearoff=0)
- editmenu.add_command(
- label="Edit Grammar",
- underline=5,
- command=self.edit_grammar,
- accelerator="Ctrl-g",
- )
- editmenu.add_command(
- label="Edit Text",
- underline=5,
- command=self.edit_sentence,
- accelerator="Ctrl-t",
- )
- menubar.add_cascade(label="Edit", underline=0, menu=editmenu)
- rulemenu = Menu(menubar, tearoff=0)
- rulemenu.add_command(
- label="Step", underline=1, command=self.step, accelerator="Space"
- )
- rulemenu.add_separator()
- rulemenu.add_command(
- label="Shift", underline=0, command=self.shift, accelerator="Ctrl-s"
- )
- rulemenu.add_command(
- label="Reduce", underline=0, command=self.reduce, accelerator="Ctrl-r"
- )
- rulemenu.add_separator()
- rulemenu.add_command(
- label="Undo", underline=0, command=self.undo, accelerator="Ctrl-u"
- )
- menubar.add_cascade(label="Apply", underline=0, menu=rulemenu)
- viewmenu = Menu(menubar, tearoff=0)
- viewmenu.add_checkbutton(
- label="Show Grammar",
- underline=0,
- variable=self._show_grammar,
- command=self._toggle_grammar,
- )
- viewmenu.add_separator()
- viewmenu.add_radiobutton(
- label="Tiny",
- variable=self._size,
- underline=0,
- value=10,
- command=self.resize,
- )
- viewmenu.add_radiobutton(
- label="Small",
- variable=self._size,
- underline=0,
- value=12,
- command=self.resize,
- )
- viewmenu.add_radiobutton(
- label="Medium",
- variable=self._size,
- underline=0,
- value=14,
- command=self.resize,
- )
- viewmenu.add_radiobutton(
- label="Large",
- variable=self._size,
- underline=0,
- value=18,
- command=self.resize,
- )
- viewmenu.add_radiobutton(
- label="Huge",
- variable=self._size,
- underline=0,
- value=24,
- command=self.resize,
- )
- menubar.add_cascade(label="View", underline=0, menu=viewmenu)
- animatemenu = Menu(menubar, tearoff=0)
- animatemenu.add_radiobutton(
- label="No Animation", underline=0, variable=self._animate, value=0
- )
- animatemenu.add_radiobutton(
- label="Slow Animation",
- underline=0,
- variable=self._animate,
- value=20,
- accelerator="-",
- )
- animatemenu.add_radiobutton(
- label="Normal Animation",
- underline=0,
- variable=self._animate,
- value=10,
- accelerator="=",
- )
- animatemenu.add_radiobutton(
- label="Fast Animation",
- underline=0,
- variable=self._animate,
- value=4,
- accelerator="+",
- )
- menubar.add_cascade(label="Animate", underline=1, menu=animatemenu)
- helpmenu = Menu(menubar, tearoff=0)
- helpmenu.add_command(label="About", underline=0, command=self.about)
- helpmenu.add_command(
- label="Instructions", underline=0, command=self.help, accelerator="F1"
- )
- menubar.add_cascade(label="Help", underline=0, menu=helpmenu)
- parent.config(menu=menubar)
- def _init_feedback(self, parent):
- self._feedbackframe = feedbackframe = Frame(parent)
- feedbackframe.pack(fill="x", side="bottom", padx=3, pady=3)
- self._lastoper_label = Label(
- feedbackframe, text="Last Operation:", font=self._font
- )
- self._lastoper_label.pack(side="left")
- lastoperframe = Frame(feedbackframe, relief="sunken", border=1)
- lastoperframe.pack(fill="x", side="right", expand=1, padx=5)
- self._lastoper1 = Label(
- lastoperframe, foreground="#007070", background="#f0f0f0", font=self._font
- )
- self._lastoper2 = Label(
- lastoperframe,
- anchor="w",
- width=30,
- foreground="#004040",
- background="#f0f0f0",
- font=self._font,
- )
- self._lastoper1.pack(side="left")
- self._lastoper2.pack(side="left", fill="x", expand=1)
- def _init_canvas(self, parent):
- self._cframe = CanvasFrame(
- parent,
- background="white",
- width=525,
- closeenough=10,
- border=2,
- relief="sunken",
- )
- self._cframe.pack(expand=1, fill="both", side="top", pady=2)
- canvas = self._canvas = self._cframe.canvas()
- self._stackwidgets = []
- self._rtextwidgets = []
- self._titlebar = canvas.create_rectangle(
- 0, 0, 0, 0, fill="#c0f0f0", outline="black"
- )
- self._exprline = canvas.create_line(0, 0, 0, 0, dash=".")
- self._stacktop = canvas.create_line(0, 0, 0, 0, fill="#408080")
- size = self._size.get() + 4
- self._stacklabel = TextWidget(
- canvas, "Stack", color="#004040", font=self._boldfont
- )
- self._rtextlabel = TextWidget(
- canvas, "Remaining Text", color="#004040", font=self._boldfont
- )
- self._cframe.add_widget(self._stacklabel)
- self._cframe.add_widget(self._rtextlabel)
- #########################################
- ## Main draw procedure
- #########################################
- def _redraw(self):
- scrollregion = self._canvas["scrollregion"].split()
- (cx1, cy1, cx2, cy2) = [int(c) for c in scrollregion]
- # Delete the old stack & rtext widgets.
- for stackwidget in self._stackwidgets:
- self._cframe.destroy_widget(stackwidget)
- self._stackwidgets = []
- for rtextwidget in self._rtextwidgets:
- self._cframe.destroy_widget(rtextwidget)
- self._rtextwidgets = []
- # Position the titlebar & exprline
- (x1, y1, x2, y2) = self._stacklabel.bbox()
- y = y2 - y1 + 10
- self._canvas.coords(self._titlebar, -5000, 0, 5000, y - 4)
- self._canvas.coords(self._exprline, 0, y * 2 - 10, 5000, y * 2 - 10)
- # Position the titlebar labels..
- (x1, y1, x2, y2) = self._stacklabel.bbox()
- self._stacklabel.move(5 - x1, 3 - y1)
- (x1, y1, x2, y2) = self._rtextlabel.bbox()
- self._rtextlabel.move(cx2 - x2 - 5, 3 - y1)
- # Draw the stack.
- stackx = 5
- for tok in self._parser.stack():
- if isinstance(tok, Tree):
- attribs = {
- "tree_color": "#4080a0",
- "tree_width": 2,
- "node_font": self._boldfont,
- "node_color": "#006060",
- "leaf_color": "#006060",
- "leaf_font": self._font,
- }
- widget = tree_to_treesegment(self._canvas, tok, **attribs)
- widget.label()["color"] = "#000000"
- else:
- widget = TextWidget(self._canvas, tok, color="#000000", font=self._font)
- widget.bind_click(self._popup_reduce)
- self._stackwidgets.append(widget)
- self._cframe.add_widget(widget, stackx, y)
- stackx = widget.bbox()[2] + 10
- # Draw the remaining text.
- rtextwidth = 0
- for tok in self._parser.remaining_text():
- widget = TextWidget(self._canvas, tok, color="#000000", font=self._font)
- self._rtextwidgets.append(widget)
- self._cframe.add_widget(widget, rtextwidth, y)
- rtextwidth = widget.bbox()[2] + 4
- # Allow enough room to shift the next token (for animations)
- if len(self._rtextwidgets) > 0:
- stackx += self._rtextwidgets[0].width()
- # Move the remaining text to the correct location (keep it
- # right-justified, when possible); and move the remaining text
- # label, if necessary.
- stackx = max(stackx, self._stacklabel.width() + 25)
- rlabelwidth = self._rtextlabel.width() + 10
- if stackx >= cx2 - max(rtextwidth, rlabelwidth):
- cx2 = stackx + max(rtextwidth, rlabelwidth)
- for rtextwidget in self._rtextwidgets:
- rtextwidget.move(4 + cx2 - rtextwidth, 0)
- self._rtextlabel.move(cx2 - self._rtextlabel.bbox()[2] - 5, 0)
- midx = (stackx + cx2 - max(rtextwidth, rlabelwidth)) / 2
- self._canvas.coords(self._stacktop, midx, 0, midx, 5000)
- (x1, y1, x2, y2) = self._stacklabel.bbox()
- # Set up binding to allow them to shift a token by dragging it.
- if len(self._rtextwidgets) > 0:
- def drag_shift(widget, midx=midx, self=self):
- if widget.bbox()[0] < midx:
- self.shift()
- else:
- self._redraw()
- self._rtextwidgets[0].bind_drag(drag_shift)
- self._rtextwidgets[0].bind_click(self.shift)
- # Draw the stack top.
- self._highlight_productions()
- def _draw_stack_top(self, widget):
- # hack..
- midx = widget.bbox()[2] + 50
- self._canvas.coords(self._stacktop, midx, 0, midx, 5000)
- def _highlight_productions(self):
- # Highlight the productions that can be reduced.
- self._prodlist.selection_clear(0, "end")
- for prod in self._parser.reducible_productions():
- index = self._productions.index(prod)
- self._prodlist.selection_set(index)
- #########################################
- ## Button Callbacks
- #########################################
- def destroy(self, *e):
- if self._top is None:
- return
- self._top.destroy()
- self._top = None
- def reset(self, *e):
- self._parser.initialize(self._sent)
- self._lastoper1["text"] = "Reset App"
- self._lastoper2["text"] = ""
- self._redraw()
- def step(self, *e):
- if self.reduce():
- return True
- elif self.shift():
- return True
- else:
- if list(self._parser.parses()):
- self._lastoper1["text"] = "Finished:"
- self._lastoper2["text"] = "Success"
- else:
- self._lastoper1["text"] = "Finished:"
- self._lastoper2["text"] = "Failure"
- def shift(self, *e):
- if self._animating_lock:
- return
- if self._parser.shift():
- tok = self._parser.stack()[-1]
- self._lastoper1["text"] = "Shift:"
- self._lastoper2["text"] = "%r" % tok
- if self._animate.get():
- self._animate_shift()
- else:
- self._redraw()
- return True
- return False
- def reduce(self, *e):
- if self._animating_lock:
- return
- production = self._parser.reduce()
- if production:
- self._lastoper1["text"] = "Reduce:"
- self._lastoper2["text"] = "%s" % production
- if self._animate.get():
- self._animate_reduce()
- else:
- self._redraw()
- return production
- def undo(self, *e):
- if self._animating_lock:
- return
- if self._parser.undo():
- self._redraw()
- def postscript(self, *e):
- self._cframe.print_to_file()
- def mainloop(self, *args, **kwargs):
- """
- Enter the Tkinter mainloop. This function must be called if
- this demo is created from a non-interactive program (e.g.
- from a secript); otherwise, the demo will close as soon as
- the script completes.
- """
- if in_idle():
- return
- self._top.mainloop(*args, **kwargs)
- #########################################
- ## Menubar callbacks
- #########################################
- def resize(self, size=None):
- if size is not None:
- self._size.set(size)
- size = self._size.get()
- self._font.configure(size=-(abs(size)))
- self._boldfont.configure(size=-(abs(size)))
- self._sysfont.configure(size=-(abs(size)))
- # self._stacklabel['font'] = ('helvetica', -size-4, 'bold')
- # self._rtextlabel['font'] = ('helvetica', -size-4, 'bold')
- # self._lastoper_label['font'] = ('helvetica', -size)
- # self._lastoper1['font'] = ('helvetica', -size)
- # self._lastoper2['font'] = ('helvetica', -size)
- # self._prodlist['font'] = ('helvetica', -size)
- # self._prodlist_label['font'] = ('helvetica', -size-2, 'bold')
- self._redraw()
- def help(self, *e):
- # The default font's not very legible; try using 'fixed' instead.
- try:
- ShowText(
- self._top,
- "Help: Shift-Reduce Parser Application",
- (__doc__ or "").strip(),
- width=75,
- font="fixed",
- )
- except:
- ShowText(
- self._top,
- "Help: Shift-Reduce Parser Application",
- (__doc__ or "").strip(),
- width=75,
- )
- def about(self, *e):
- ABOUT = "NLTK Shift-Reduce Parser Application\n" + "Written by Edward Loper"
- TITLE = "About: Shift-Reduce Parser Application"
- try:
- from tkinter.messagebox import Message
- Message(message=ABOUT, title=TITLE).show()
- except:
- ShowText(self._top, TITLE, ABOUT)
- def edit_grammar(self, *e):
- CFGEditor(self._top, self._parser.grammar(), self.set_grammar)
- def set_grammar(self, grammar):
- self._parser.set_grammar(grammar)
- self._productions = list(grammar.productions())
- self._prodlist.delete(0, "end")
- for production in self._productions:
- self._prodlist.insert("end", (" %s" % production))
- def edit_sentence(self, *e):
- sentence = " ".join(self._sent)
- title = "Edit Text"
- instr = "Enter a new sentence to parse."
- EntryDialog(self._top, sentence, instr, self.set_sentence, title)
- def set_sentence(self, sent):
- self._sent = sent.split() # [XX] use tagged?
- self.reset()
- #########################################
- ## Reduce Production Selection
- #########################################
- def _toggle_grammar(self, *e):
- if self._show_grammar.get():
- self._prodframe.pack(
- fill="both", side="left", padx=2, after=self._feedbackframe
- )
- self._lastoper1["text"] = "Show Grammar"
- else:
- self._prodframe.pack_forget()
- self._lastoper1["text"] = "Hide Grammar"
- self._lastoper2["text"] = ""
- def _prodlist_select(self, event):
- selection = self._prodlist.curselection()
- if len(selection) != 1:
- return
- index = int(selection[0])
- production = self._parser.reduce(self._productions[index])
- if production:
- self._lastoper1["text"] = "Reduce:"
- self._lastoper2["text"] = "%s" % production
- if self._animate.get():
- self._animate_reduce()
- else:
- self._redraw()
- else:
- # Reset the production selections.
- self._prodlist.selection_clear(0, "end")
- for prod in self._parser.reducible_productions():
- index = self._productions.index(prod)
- self._prodlist.selection_set(index)
- def _popup_reduce(self, widget):
- # Remove old commands.
- productions = self._parser.reducible_productions()
- if len(productions) == 0:
- return
- self._reduce_menu.delete(0, "end")
- for production in productions:
- self._reduce_menu.add_command(label=str(production), command=self.reduce)
- self._reduce_menu.post(
- self._canvas.winfo_pointerx(), self._canvas.winfo_pointery()
- )
- #########################################
- ## Animations
- #########################################
- def _animate_shift(self):
- # What widget are we shifting?
- widget = self._rtextwidgets[0]
- # Where are we shifting from & to?
- right = widget.bbox()[0]
- if len(self._stackwidgets) == 0:
- left = 5
- else:
- left = self._stackwidgets[-1].bbox()[2] + 10
- # Start animating.
- dt = self._animate.get()
- dx = (left - right) * 1.0 / dt
- self._animate_shift_frame(dt, widget, dx)
- def _animate_shift_frame(self, frame, widget, dx):
- if frame > 0:
- self._animating_lock = 1
- widget.move(dx, 0)
- self._top.after(10, self._animate_shift_frame, frame - 1, widget, dx)
- else:
- # but: stacktop??
- # Shift the widget to the stack.
- del self._rtextwidgets[0]
- self._stackwidgets.append(widget)
- self._animating_lock = 0
- # Display the available productions.
- self._draw_stack_top(widget)
- self._highlight_productions()
- def _animate_reduce(self):
- # What widgets are we shifting?
- numwidgets = len(self._parser.stack()[-1]) # number of children
- widgets = self._stackwidgets[-numwidgets:]
- # How far are we moving?
- if isinstance(widgets[0], TreeSegmentWidget):
- ydist = 15 + widgets[0].label().height()
- else:
- ydist = 15 + widgets[0].height()
- # Start animating.
- dt = self._animate.get()
- dy = ydist * 2.0 / dt
- self._animate_reduce_frame(dt / 2, widgets, dy)
- def _animate_reduce_frame(self, frame, widgets, dy):
- if frame > 0:
- self._animating_lock = 1
- for widget in widgets:
- widget.move(0, dy)
- self._top.after(10, self._animate_reduce_frame, frame - 1, widgets, dy)
- else:
- del self._stackwidgets[-len(widgets) :]
- for widget in widgets:
- self._cframe.remove_widget(widget)
- tok = self._parser.stack()[-1]
- if not isinstance(tok, Tree):
- raise ValueError()
- label = TextWidget(
- self._canvas, str(tok.label()), color="#006060", font=self._boldfont
- )
- widget = TreeSegmentWidget(self._canvas, label, widgets, width=2)
- (x1, y1, x2, y2) = self._stacklabel.bbox()
- y = y2 - y1 + 10
- if not self._stackwidgets:
- x = 5
- else:
- x = self._stackwidgets[-1].bbox()[2] + 10
- self._cframe.add_widget(widget, x, y)
- self._stackwidgets.append(widget)
- # Display the available productions.
- self._draw_stack_top(widget)
- self._highlight_productions()
- # # Delete the old widgets..
- # del self._stackwidgets[-len(widgets):]
- # for widget in widgets:
- # self._cframe.destroy_widget(widget)
- #
- # # Make a new one.
- # tok = self._parser.stack()[-1]
- # if isinstance(tok, Tree):
- # attribs = {'tree_color': '#4080a0', 'tree_width': 2,
- # 'node_font': bold, 'node_color': '#006060',
- # 'leaf_color': '#006060', 'leaf_font':self._font}
- # widget = tree_to_treesegment(self._canvas, tok.type(),
- # **attribs)
- # widget.node()['color'] = '#000000'
- # else:
- # widget = TextWidget(self._canvas, tok.type(),
- # color='#000000', font=self._font)
- # widget.bind_click(self._popup_reduce)
- # (x1, y1, x2, y2) = self._stacklabel.bbox()
- # y = y2-y1+10
- # if not self._stackwidgets: x = 5
- # else: x = self._stackwidgets[-1].bbox()[2] + 10
- # self._cframe.add_widget(widget, x, y)
- # self._stackwidgets.append(widget)
- # self._redraw()
- self._animating_lock = 0
- #########################################
- ## Hovering.
- #########################################
- def _highlight_hover(self, event):
- # What production are we hovering over?
- index = self._prodlist.nearest(event.y)
- if self._hover == index:
- return
- # Clear any previous hover highlighting.
- self._clear_hover()
- # If the production corresponds to an available reduction,
- # highlight the stack.
- selection = [int(s) for s in self._prodlist.curselection()]
- if index in selection:
- rhslen = len(self._productions[index].rhs())
- for stackwidget in self._stackwidgets[-rhslen:]:
- if isinstance(stackwidget, TreeSegmentWidget):
- stackwidget.label()["color"] = "#00a000"
- else:
- stackwidget["color"] = "#00a000"
- # Remember what production we're hovering over.
- self._hover = index
- def _clear_hover(self, *event):
- # Clear any previous hover highlighting.
- if self._hover == -1:
- return
- self._hover = -1
- for stackwidget in self._stackwidgets:
- if isinstance(stackwidget, TreeSegmentWidget):
- stackwidget.label()["color"] = "black"
- else:
- stackwidget["color"] = "black"
- def app():
- """
- Create a shift reduce parser app, using a simple grammar and
- text.
- """
- from nltk.grammar import Nonterminal, Production, CFG
- nonterminals = "S VP NP PP P N Name V Det"
- (S, VP, NP, PP, P, N, Name, V, Det) = [Nonterminal(s) for s in nonterminals.split()]
- productions = (
- # Syntactic Productions
- Production(S, [NP, VP]),
- Production(NP, [Det, N]),
- Production(NP, [NP, PP]),
- Production(VP, [VP, PP]),
- Production(VP, [V, NP, PP]),
- Production(VP, [V, NP]),
- Production(PP, [P, NP]),
- # Lexical Productions
- Production(NP, ["I"]),
- Production(Det, ["the"]),
- Production(Det, ["a"]),
- Production(N, ["man"]),
- Production(V, ["saw"]),
- Production(P, ["in"]),
- Production(P, ["with"]),
- Production(N, ["park"]),
- Production(N, ["dog"]),
- Production(N, ["statue"]),
- Production(Det, ["my"]),
- )
- grammar = CFG(S, productions)
- # tokenize the sentence
- sent = "my dog saw a man in the park with a statue".split()
- ShiftReduceApp(grammar, sent).mainloop()
- if __name__ == "__main__":
- app()
- __all__ = ["app"]
|