You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
938 lines
33 KiB
Python
938 lines
33 KiB
Python
# Natural Language Toolkit: Shift-Reduce Parser Application
|
|
#
|
|
# Copyright (C) 2001-2019 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 six.moves.tkinter_font import Font
|
|
from six.moves.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 six.moves.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']
|