810 lines
32 KiB
Python
810 lines
32 KiB
Python
|
# Natural Language Toolkit: Shift-Reduce Parser Application
|
||
|
#
|
||
|
# Copyright (C) 2001-2018 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']
|