Spaces:
Sleeping
Sleeping
# Natural Language Toolkit: Shift-Reduce Parser Application | |
# | |
# Copyright (C) 2001-2023 NLTK Project | |
# Author: Edward Loper <[email protected]> | |
# URL: <https://www.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 import Button, Frame, IntVar, Label, Listbox, Menu, Scrollbar, Tk | |
from tkinter.font import Font | |
from nltk.draw import CFGEditor, TreeSegmentWidget, tree_to_treesegment | |
from nltk.draw.util import CanvasFrame, EntryDialog, ShowText, TextWidget | |
from nltk.parse import SteppingShiftReduceParser | |
from nltk.tree import Tree | |
from nltk.util import in_idle | |
""" | |
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: | |
""" | |
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 CFG, Nonterminal, Production | |
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"] | |