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.

1053 lines
36 KiB
Python

4 years ago
# Natural Language Toolkit: Recursive Descent Parser Application
#
# Copyright (C) 2001-2020 NLTK Project
4 years ago
# Author: Edward Loper <edloper@gmail.com>
# URL: <http://nltk.org/>
# For license information, see LICENSE.TXT
"""
A graphical tool for exploring the recursive descent parser.
The recursive descent parser maintains a tree, which records the
structure of the portion of the text that has been parsed. It uses
CFG productions to expand the fringe of the tree, and matches its
leaves against the text. Initially, the tree contains the start
symbol ("S"). It is shown in the main canvas, to the right of the
list of available expansions.
The parser builds up a tree structure for the text using three
operations:
- "expand" uses a CFG production to add children to a node on the
fringe of the tree.
- "match" compares a leaf in the tree to a text token.
- "backtrack" returns the tree to its state before the most recent
expand or match operation.
The parser maintains a list of tree locations called a "frontier" to
remember which nodes have not yet been expanded and which leaves have
not yet been matched against the text. The leftmost frontier node is
shown in green, and the other frontier nodes are shown in blue. The
parser always performs expand and match operations on the leftmost
element of the frontier.
You can control the parser's operation by using the "expand," "match,"
and "backtrack" 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:
- If the leftmost frontier element is a token, try matching it.
- If the leftmost frontier element is a node, try expanding it with
the first untried expansion.
- Otherwise, backtrack.
The "expand" button applies the untried expansion whose CFG production
is listed earliest in the grammar. To manually choose which expansion
to apply, click on a CFG production from the list of available
expansions, on the left side of the main window.
The "autostep" button will let the parser continue applying
applications to the tree until it reaches a complete parse. You can
cancel an autostep in progress at any time by clicking on the
"autostep" button again.
Keyboard Shortcuts::
[Space]\t Perform the next expand, match, or backtrack operation
[a]\t Step through operations until the next complete parse
[e]\t Perform an expand operation
[m]\t Perform a match operation
[b]\t Perform a backtrack operation
[Delete]\t Reset the parser
[g]\t Show/hide available expansions list
[h]\t Help
[Ctrl-p]\t Print
[q]\t Quit
"""
from tkinter.font import Font
from tkinter import Listbox, IntVar, Button, Frame, Label, Menu, Scrollbar, Tk
4 years ago
from nltk.tree import Tree
from nltk.util import in_idle
from nltk.parse import SteppingRecursiveDescentParser
from nltk.draw.util import TextWidget, ShowText, CanvasFrame, EntryDialog
from nltk.draw import CFGEditor, TreeSegmentWidget, tree_to_treesegment
class RecursiveDescentApp(object):
"""
A graphical tool for exploring the recursive descent parser. The tool
displays the parser's tree and the remaining text, and allows the
user to control the parser's operation. In particular, the user
can expand subtrees on the frontier, match tokens on the frontier
against the text, and backtrack. A "step" button simply steps
through the parsing process, performing the operations that
``RecursiveDescentParser`` would use.
"""
def __init__(self, grammar, sent, trace=0):
self._sent = sent
self._parser = SteppingRecursiveDescentParser(grammar, trace)
# Set up the main window.
self._top = Tk()
self._top.title("Recursive Descent Parser Application")
4 years ago
# Set up key bindings.
self._init_bindings()
# Initialize the fonts.
self._init_fonts(self._top)
# Animations. animating_lock is a lock to prevent the demo
# from performing new operations while it's animating.
self._animation_frames = IntVar(self._top)
self._animation_frames.set(5)
self._animating_lock = 0
self._autostep = 0
# The user can hide the grammar.
self._show_grammar = IntVar(self._top)
self._show_grammar.set(1)
# 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)
# Initialize the parser.
self._parser.initialize(self._sent)
# Resize callback
self._canvas.bind("<Configure>", self._configure)
4 years ago
#########################################
## 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"))
4 years ago
self._boldfont = Font(family="helvetica", weight="bold", size=self._size.get())
self._font = Font(family="helvetica", size=self._size.get())
4 years ago
if self._size.get() < 0:
big = self._size.get() - 2
else:
big = self._size.get() + 2
self._bigfont = Font(family="helvetica", weight="bold", size=big)
4 years ago
def _init_grammar(self, parent):
# Grammar view.
self._prodframe = listframe = Frame(parent)
self._prodframe.pack(fill="both", side="left", padx=2)
4 years ago
self._prodlist_label = Label(
self._prodframe, font=self._boldfont, text="Available Expansions"
4 years ago
)
self._prodlist_label.pack()
self._prodlist = Listbox(
self._prodframe,
selectmode="single",
relief="groove",
background="white",
foreground="#909090",
4 years ago
font=self._font,
selectforeground="#004040",
selectbackground="#c0f0c0",
4 years ago
)
self._prodlist.pack(side="right", fill="both", expand=1)
4 years ago
self._productions = list(self._parser.grammar().productions())
for production in self._productions:
self._prodlist.insert("end", (" %s" % production))
4 years ago
self._prodlist.config(height=min(len(self._productions), 25))
# Add a scrollbar if there are more than 25 productions.
if len(self._productions) > 25:
listscroll = Scrollbar(self._prodframe, orient="vertical")
4 years ago
self._prodlist.config(yscrollcommand=listscroll.set)
listscroll.config(command=self._prodlist.yview)
listscroll.pack(side="left", fill="y")
4 years ago
# If they select a production, apply it.
self._prodlist.bind("<<ListboxSelect>>", self._prodlist_select)
4 years ago
def _init_bindings(self):
# Key bindings are a good thing.
self._top.bind("<Control-q>", self.destroy)
self._top.bind("<Control-x>", self.destroy)
self._top.bind("<Escape>", self.destroy)
self._top.bind("e", self.expand)
4 years ago
# self._top.bind('<Alt-e>', self.expand)
# self._top.bind('<Control-e>', self.expand)
self._top.bind("m", self.match)
self._top.bind("<Alt-m>", self.match)
self._top.bind("<Control-m>", self.match)
self._top.bind("b", self.backtrack)
self._top.bind("<Alt-b>", self.backtrack)
self._top.bind("<Control-b>", self.backtrack)
self._top.bind("<Control-z>", self.backtrack)
self._top.bind("<BackSpace>", self.backtrack)
self._top.bind("a", self.autostep)
4 years ago
# self._top.bind('<Control-a>', self.autostep)
self._top.bind("<Control-space>", self.autostep)
self._top.bind("<Control-c>", self.cancel_autostep)
self._top.bind("<space>", self.step)
self._top.bind("<Delete>", self.reset)
self._top.bind("<Control-p>", self.postscript)
4 years ago
# self._top.bind('<h>', self.help)
# self._top.bind('<Alt-h>', self.help)
self._top.bind("<Control-h>", self.help)
self._top.bind("<F1>", self.help)
4 years ago
# self._top.bind('<g>', self.toggle_grammar)
# self._top.bind('<Alt-g>', self.toggle_grammar)
# self._top.bind('<Control-g>', self.toggle_grammar)
self._top.bind("<Control-g>", self.edit_grammar)
self._top.bind("<Control-t>", self.edit_sentence)
4 years ago
def _init_buttons(self, parent):
# Set up the frames.
self._buttonframe = buttonframe = Frame(parent)
buttonframe.pack(fill="none", side="bottom", padx=3, pady=2)
4 years ago
Button(
buttonframe,
text="Step",
background="#90c0d0",
foreground="black",
4 years ago
command=self.step,
).pack(side="left")
4 years ago
Button(
buttonframe,
text="Autostep",
background="#90c0d0",
foreground="black",
4 years ago
command=self.autostep,
).pack(side="left")
4 years ago
Button(
buttonframe,
text="Expand",
4 years ago
underline=0,
background="#90f090",
foreground="black",
4 years ago
command=self.expand,
).pack(side="left")
4 years ago
Button(
buttonframe,
text="Match",
4 years ago
underline=0,
background="#90f090",
foreground="black",
4 years ago
command=self.match,
).pack(side="left")
4 years ago
Button(
buttonframe,
text="Backtrack",
4 years ago
underline=0,
background="#f0a0a0",
foreground="black",
4 years ago
command=self.backtrack,
).pack(side="left")
4 years ago
# Replace autostep...
# self._autostep_button = Button(buttonframe, text='Autostep',
# underline=0, command=self.autostep)
# self._autostep_button.pack(side='left')
def _configure(self, event):
self._autostep = 0
(x1, y1, x2, y2) = self._cframe.scrollregion()
y2 = event.height - 6
self._canvas["scrollregion"] = "%d %d %d %d" % (x1, y1, x2, y2)
4 years ago
self._redraw()
def _init_feedback(self, parent):
self._feedbackframe = feedbackframe = Frame(parent)
feedbackframe.pack(fill="x", side="bottom", padx=3, pady=3)
4 years ago
self._lastoper_label = Label(
feedbackframe, text="Last Operation:", font=self._font
4 years ago
)
self._lastoper_label.pack(side="left")
lastoperframe = Frame(feedbackframe, relief="sunken", border=1)
lastoperframe.pack(fill="x", side="right", expand=1, padx=5)
4 years ago
self._lastoper1 = Label(
lastoperframe, foreground="#007070", background="#f0f0f0", font=self._font
4 years ago
)
self._lastoper2 = Label(
lastoperframe,
anchor="w",
4 years ago
width=30,
foreground="#004040",
background="#f0f0f0",
4 years ago
font=self._font,
)
self._lastoper1.pack(side="left")
self._lastoper2.pack(side="left", fill="x", expand=1)
4 years ago
def _init_canvas(self, parent):
self._cframe = CanvasFrame(
parent,
background="white",
4 years ago
# width=525, height=250,
closeenough=10,
border=2,
relief="sunken",
4 years ago
)
self._cframe.pack(expand=1, fill="both", side="top", pady=2)
4 years ago
canvas = self._canvas = self._cframe.canvas()
# Initially, there's no tree or text
self._tree = None
self._textwidgets = []
self._textline = None
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"
4 years ago
)
filemenu.add_command(
label="Print to Postscript",
4 years ago
underline=0,
command=self.postscript,
accelerator="Ctrl-p",
4 years ago
)
filemenu.add_command(
label="Exit", underline=1, command=self.destroy, accelerator="Ctrl-x"
4 years ago
)
menubar.add_cascade(label="File", underline=0, menu=filemenu)
4 years ago
editmenu = Menu(menubar, tearoff=0)
editmenu.add_command(
label="Edit Grammar",
4 years ago
underline=5,
command=self.edit_grammar,
accelerator="Ctrl-g",
4 years ago
)
editmenu.add_command(
label="Edit Text",
4 years ago
underline=5,
command=self.edit_sentence,
accelerator="Ctrl-t",
4 years ago
)
menubar.add_cascade(label="Edit", underline=0, menu=editmenu)
4 years ago
rulemenu = Menu(menubar, tearoff=0)
rulemenu.add_command(
label="Step", underline=1, command=self.step, accelerator="Space"
4 years ago
)
rulemenu.add_separator()
rulemenu.add_command(
label="Match", underline=0, command=self.match, accelerator="Ctrl-m"
4 years ago
)
rulemenu.add_command(
label="Expand", underline=0, command=self.expand, accelerator="Ctrl-e"
4 years ago
)
rulemenu.add_separator()
rulemenu.add_command(
label="Backtrack", underline=0, command=self.backtrack, accelerator="Ctrl-b"
4 years ago
)
menubar.add_cascade(label="Apply", underline=0, menu=rulemenu)
4 years ago
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",
4 years ago
variable=self._size,
underline=0,
value=10,
command=self.resize,
)
viewmenu.add_radiobutton(
label="Small",
4 years ago
variable=self._size,
underline=0,
value=12,
command=self.resize,
)
viewmenu.add_radiobutton(
label="Medium",
4 years ago
variable=self._size,
underline=0,
value=14,
command=self.resize,
)
viewmenu.add_radiobutton(
label="Large",
4 years ago
variable=self._size,
underline=0,
value=18,
command=self.resize,
)
viewmenu.add_radiobutton(
label="Huge",
4 years ago
variable=self._size,
underline=0,
value=24,
command=self.resize,
)
menubar.add_cascade(label="View", underline=0, menu=viewmenu)
4 years ago
animatemenu = Menu(menubar, tearoff=0)
animatemenu.add_radiobutton(
label="No Animation", underline=0, variable=self._animation_frames, value=0
)
animatemenu.add_radiobutton(
label="Slow Animation",
underline=0,
variable=self._animation_frames,
value=10,
accelerator="-",
4 years ago
)
animatemenu.add_radiobutton(
label="Normal Animation",
underline=0,
variable=self._animation_frames,
value=5,
accelerator="=",
4 years ago
)
animatemenu.add_radiobutton(
label="Fast Animation",
underline=0,
variable=self._animation_frames,
value=2,
accelerator="+",
4 years ago
)
menubar.add_cascade(label="Animate", underline=1, menu=animatemenu)
helpmenu = Menu(menubar, tearoff=0)
helpmenu.add_command(label="About", underline=0, command=self.about)
4 years ago
helpmenu.add_command(
label="Instructions", underline=0, command=self.help, accelerator="F1"
4 years ago
)
menubar.add_cascade(label="Help", underline=0, menu=helpmenu)
4 years ago
parent.config(menu=menubar)
#########################################
## Helper
#########################################
def _get(self, widget, treeloc):
for i in treeloc:
widget = widget.subtrees()[i]
if isinstance(widget, TreeSegmentWidget):
widget = widget.label()
return widget
#########################################
## Main draw procedure
#########################################
def _redraw(self):
canvas = self._canvas
# Delete the old tree, widgets, etc.
if self._tree is not None:
self._cframe.destroy_widget(self._tree)
for twidget in self._textwidgets:
self._cframe.destroy_widget(twidget)
if self._textline is not None:
self._canvas.delete(self._textline)
# Draw the tree.
helv = ("helvetica", -self._size.get())
bold = ("helvetica", -self._size.get(), "bold")
4 years ago
attribs = {
"tree_color": "#000000",
"tree_width": 2,
"node_font": bold,
"leaf_font": helv,
4 years ago
}
tree = self._parser.tree()
self._tree = tree_to_treesegment(canvas, tree, **attribs)
self._cframe.add_widget(self._tree, 30, 5)
# Draw the text.
helv = ("helvetica", -self._size.get())
4 years ago
bottom = y = self._cframe.scrollregion()[3]
self._textwidgets = [
TextWidget(canvas, word, font=self._font) for word in self._sent
]
for twidget in self._textwidgets:
self._cframe.add_widget(twidget, 0, 0)
twidget.move(0, bottom - twidget.bbox()[3] - 5)
y = min(y, twidget.bbox()[1])
# Draw a line over the text, to separate it from the tree.
self._textline = canvas.create_line(-5000, y - 5, 5000, y - 5, dash=".")
4 years ago
# Highlight appropriate nodes.
self._highlight_nodes()
self._highlight_prodlist()
# Make sure the text lines up.
self._position_text()
def _redraw_quick(self):
# This should be more-or-less sufficient after an animation.
self._highlight_nodes()
self._highlight_prodlist()
self._position_text()
def _highlight_nodes(self):
# Highlight the list of nodes to be checked.
bold = ("helvetica", -self._size.get(), "bold")
4 years ago
for treeloc in self._parser.frontier()[:1]:
self._get(self._tree, treeloc)["color"] = "#20a050"
self._get(self._tree, treeloc)["font"] = bold
4 years ago
for treeloc in self._parser.frontier()[1:]:
self._get(self._tree, treeloc)["color"] = "#008080"
4 years ago
def _highlight_prodlist(self):
# Highlight the productions that can be expanded.
# Boy, too bad tkinter doesn't implement Listbox.itemconfig;
# that would be pretty useful here.
self._prodlist.delete(0, "end")
4 years ago
expandable = self._parser.expandable_productions()
untried = self._parser.untried_expandable_productions()
productions = self._productions
for index in range(len(productions)):
if productions[index] in expandable:
if productions[index] in untried:
self._prodlist.insert(index, " %s" % productions[index])
4 years ago
else:
self._prodlist.insert(index, " %s (TRIED)" % productions[index])
4 years ago
self._prodlist.selection_set(index)
else:
self._prodlist.insert(index, " %s" % productions[index])
4 years ago
def _position_text(self):
# Line up the text widgets that are matched against the tree
numwords = len(self._sent)
num_matched = numwords - len(self._parser.remaining_text())
leaves = self._tree_leaves()[:num_matched]
xmax = self._tree.bbox()[0]
for i in range(0, len(leaves)):
widget = self._textwidgets[i]
leaf = leaves[i]
widget["color"] = "#006040"
leaf["color"] = "#006040"
4 years ago
widget.move(leaf.bbox()[0] - widget.bbox()[0], 0)
xmax = widget.bbox()[2] + 10
# Line up the text widgets that are not matched against the tree.
for i in range(len(leaves), numwords):
widget = self._textwidgets[i]
widget["color"] = "#a0a0a0"
4 years ago
widget.move(xmax - widget.bbox()[0], 0)
xmax = widget.bbox()[2] + 10
# If we have a complete parse, make everything green :)
if self._parser.currently_complete():
for twidget in self._textwidgets:
twidget["color"] = "#00a000"
4 years ago
# Move the matched leaves down to the text.
for i in range(0, len(leaves)):
widget = self._textwidgets[i]
leaf = leaves[i]
dy = widget.bbox()[1] - leaf.bbox()[3] - 10.0
dy = max(dy, leaf.parent().label().bbox()[3] - leaf.bbox()[3] + 10)
leaf.move(0, dy)
def _tree_leaves(self, tree=None):
if tree is None:
tree = self._tree
if isinstance(tree, TreeSegmentWidget):
leaves = []
for child in tree.subtrees():
leaves += self._tree_leaves(child)
return leaves
else:
return [tree]
#########################################
## Button Callbacks
#########################################
def destroy(self, *e):
self._autostep = 0
if self._top is None:
return
self._top.destroy()
self._top = None
def reset(self, *e):
self._autostep = 0
self._parser.initialize(self._sent)
self._lastoper1["text"] = "Reset Application"
self._lastoper2["text"] = ""
4 years ago
self._redraw()
def autostep(self, *e):
if self._animation_frames.get() == 0:
self._animation_frames.set(2)
if self._autostep:
self._autostep = 0
else:
self._autostep = 1
self._step()
def cancel_autostep(self, *e):
# self._autostep_button['text'] = 'Autostep'
self._autostep = 0
# Make sure to stop auto-stepping if we get any user input.
def step(self, *e):
self._autostep = 0
self._step()
def match(self, *e):
self._autostep = 0
self._match()
def expand(self, *e):
self._autostep = 0
self._expand()
def backtrack(self, *e):
self._autostep = 0
self._backtrack()
def _step(self):
if self._animating_lock:
return
# Try expanding, matching, and backtracking (in that order)
if self._expand():
pass
elif self._parser.untried_match() and self._match():
pass
elif self._backtrack():
pass
else:
self._lastoper1["text"] = "Finished"
self._lastoper2["text"] = ""
4 years ago
self._autostep = 0
# Check if we just completed a parse.
if self._parser.currently_complete():
self._autostep = 0
self._lastoper2["text"] += " [COMPLETE PARSE]"
4 years ago
def _expand(self, *e):
if self._animating_lock:
return
old_frontier = self._parser.frontier()
rv = self._parser.expand()
if rv is not None:
self._lastoper1["text"] = "Expand:"
self._lastoper2["text"] = rv
self._prodlist.selection_clear(0, "end")
4 years ago
index = self._productions.index(rv)
self._prodlist.selection_set(index)
self._animate_expand(old_frontier[0])
return True
else:
self._lastoper1["text"] = "Expand:"
self._lastoper2["text"] = "(all expansions tried)"
4 years ago
return False
def _match(self, *e):
if self._animating_lock:
return
old_frontier = self._parser.frontier()
rv = self._parser.match()
if rv is not None:
self._lastoper1["text"] = "Match:"
self._lastoper2["text"] = rv
4 years ago
self._animate_match(old_frontier[0])
return True
else:
self._lastoper1["text"] = "Match:"
self._lastoper2["text"] = "(failed)"
4 years ago
return False
def _backtrack(self, *e):
if self._animating_lock:
return
if self._parser.backtrack():
elt = self._parser.tree()
for i in self._parser.frontier()[0]:
elt = elt[i]
self._lastoper1["text"] = "Backtrack"
self._lastoper2["text"] = ""
4 years ago
if isinstance(elt, Tree):
self._animate_backtrack(self._parser.frontier()[0])
else:
self._animate_match_backtrack(self._parser.frontier()[0])
return True
else:
self._autostep = 0
self._lastoper1["text"] = "Finished"
self._lastoper2["text"] = ""
4 years ago
return False
def about(self, *e):
ABOUT = (
"NLTK Recursive Descent Parser Application\n" + "Written by Edward Loper"
)
TITLE = "About: Recursive Descent Parser Application"
4 years ago
try:
from tkinter.messagebox import Message
4 years ago
Message(message=ABOUT, title=TITLE).show()
except:
ShowText(self._top, TITLE, ABOUT)
def help(self, *e):
self._autostep = 0
# The default font's not very legible; try using 'fixed' instead.
try:
ShowText(
self._top,
"Help: Recursive Descent Parser Application",
(__doc__ or "").strip(),
4 years ago
width=75,
font="fixed",
4 years ago
)
except:
ShowText(
self._top,
"Help: Recursive Descent Parser Application",
(__doc__ or "").strip(),
4 years ago
width=75,
)
def postscript(self, *e):
self._autostep = 0
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)
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._bigfont.configure(size=-(abs(size + 2)))
self._redraw()
#########################################
## Expand Production Selection
#########################################
def _toggle_grammar(self, *e):
if self._show_grammar.get():
self._prodframe.pack(
fill="both", side="left", padx=2, after=self._feedbackframe
4 years ago
)
self._lastoper1["text"] = "Show Grammar"
4 years ago
else:
self._prodframe.pack_forget()
self._lastoper1["text"] = "Hide Grammar"
self._lastoper2["text"] = ""
4 years ago
# def toggle_grammar(self, *e):
# self._show_grammar = not self._show_grammar
# if self._show_grammar:
# self._prodframe.pack(fill='both', expand='y', side='left',
# 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])
old_frontier = self._parser.frontier()
production = self._parser.expand(self._productions[index])
if production:
self._lastoper1["text"] = "Expand:"
self._lastoper2["text"] = production
self._prodlist.selection_clear(0, "end")
4 years ago
self._prodlist.selection_set(index)
self._animate_expand(old_frontier[0])
else:
# Reset the production selections.
self._prodlist.selection_clear(0, "end")
4 years ago
for prod in self._parser.expandable_productions():
index = self._productions.index(prod)
self._prodlist.selection_set(index)
#########################################
## Animation
#########################################
def _animate_expand(self, treeloc):
oldwidget = self._get(self._tree, treeloc)
oldtree = oldwidget.parent()
top = not isinstance(oldtree.parent(), TreeSegmentWidget)
tree = self._parser.tree()
for i in treeloc:
tree = tree[i]
widget = tree_to_treesegment(
self._canvas,
tree,
node_font=self._boldfont,
leaf_color="white",
4 years ago
tree_width=2,
tree_color="white",
node_color="white",
4 years ago
leaf_font=self._font,
)
widget.label()["color"] = "#20a050"
4 years ago
(oldx, oldy) = oldtree.label().bbox()[:2]
(newx, newy) = widget.label().bbox()[:2]
widget.move(oldx - newx, oldy - newy)
if top:
self._cframe.add_widget(widget, 0, 5)
widget.move(30 - widget.label().bbox()[0], 0)
self._tree = widget
else:
oldtree.parent().replace_child(oldtree, widget)
# Move the children over so they don't overlap.
# Line the children up in a strange way.
if widget.subtrees():
dx = (
oldx
+ widget.label().width() / 2
- widget.subtrees()[0].bbox()[0] / 2
- widget.subtrees()[0].bbox()[2] / 2
)
for subtree in widget.subtrees():
subtree.move(dx, 0)
self._makeroom(widget)
if top:
self._cframe.destroy_widget(oldtree)
else:
oldtree.destroy()
colors = [
"gray%d" % (10 * int(10 * x / self._animation_frames.get()))
4 years ago
for x in range(self._animation_frames.get(), 0, -1)
]
# Move the text string down, if necessary.
dy = widget.bbox()[3] + 30 - self._canvas.coords(self._textline)[1]
if dy > 0:
for twidget in self._textwidgets:
twidget.move(0, dy)
self._canvas.move(self._textline, 0, dy)
self._animate_expand_frame(widget, colors)
def _makeroom(self, treeseg):
"""
Make sure that no sibling tree bbox's overlap.
"""
parent = treeseg.parent()
if not isinstance(parent, TreeSegmentWidget):
return
index = parent.subtrees().index(treeseg)
# Handle siblings to the right
rsiblings = parent.subtrees()[index + 1 :]
if rsiblings:
dx = treeseg.bbox()[2] - rsiblings[0].bbox()[0] + 10
for sibling in rsiblings:
sibling.move(dx, 0)
# Handle siblings to the left
if index > 0:
lsibling = parent.subtrees()[index - 1]
dx = max(0, lsibling.bbox()[2] - treeseg.bbox()[0] + 10)
treeseg.move(dx, 0)
# Keep working up the tree.
self._makeroom(parent)
def _animate_expand_frame(self, widget, colors):
if len(colors) > 0:
self._animating_lock = 1
widget["color"] = colors[0]
4 years ago
for subtree in widget.subtrees():
if isinstance(subtree, TreeSegmentWidget):
subtree.label()["color"] = colors[0]
4 years ago
else:
subtree["color"] = colors[0]
4 years ago
self._top.after(50, self._animate_expand_frame, widget, colors[1:])
else:
widget["color"] = "black"
4 years ago
for subtree in widget.subtrees():
if isinstance(subtree, TreeSegmentWidget):
subtree.label()["color"] = "black"
4 years ago
else:
subtree["color"] = "black"
4 years ago
self._redraw_quick()
widget.label()["color"] = "black"
4 years ago
self._animating_lock = 0
if self._autostep:
self._step()
def _animate_backtrack(self, treeloc):
# Flash red first, if we're animating.
if self._animation_frames.get() == 0:
colors = []
else:
colors = ["#a00000", "#000000", "#a00000"]
4 years ago
colors += [
"gray%d" % (10 * int(10 * x / (self._animation_frames.get())))
4 years ago
for x in range(1, self._animation_frames.get() + 1)
]
widgets = [self._get(self._tree, treeloc).parent()]
for subtree in widgets[0].subtrees():
if isinstance(subtree, TreeSegmentWidget):
widgets.append(subtree.label())
else:
widgets.append(subtree)
self._animate_backtrack_frame(widgets, colors)
def _animate_backtrack_frame(self, widgets, colors):
if len(colors) > 0:
self._animating_lock = 1
for widget in widgets:
widget["color"] = colors[0]
4 years ago
self._top.after(50, self._animate_backtrack_frame, widgets, colors[1:])
else:
for widget in widgets[0].subtrees():
widgets[0].remove_child(widget)
widget.destroy()
self._redraw_quick()
self._animating_lock = 0
if self._autostep:
self._step()
def _animate_match_backtrack(self, treeloc):
widget = self._get(self._tree, treeloc)
node = widget.parent().label()
dy = (node.bbox()[3] - widget.bbox()[1] + 14) / max(
1, self._animation_frames.get()
)
self._animate_match_backtrack_frame(self._animation_frames.get(), widget, dy)
def _animate_match(self, treeloc):
widget = self._get(self._tree, treeloc)
dy = (self._textwidgets[0].bbox()[1] - widget.bbox()[3] - 10.0) / max(
1, self._animation_frames.get()
)
self._animate_match_frame(self._animation_frames.get(), widget, dy)
def _animate_match_frame(self, frame, widget, dy):
if frame > 0:
self._animating_lock = 1
widget.move(0, dy)
self._top.after(10, self._animate_match_frame, frame - 1, widget, dy)
else:
widget["color"] = "#006040"
4 years ago
self._redraw_quick()
self._animating_lock = 0
if self._autostep:
self._step()
def _animate_match_backtrack_frame(self, frame, widget, dy):
if frame > 0:
self._animating_lock = 1
widget.move(0, dy)
self._top.after(
10, self._animate_match_backtrack_frame, frame - 1, widget, dy
)
else:
widget.parent().remove_child(widget)
widget.destroy()
self._animating_lock = 0
if self._autostep:
self._step()
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")
4 years ago
for production in self._productions:
self._prodlist.insert("end", (" %s" % production))
4 years ago
def edit_sentence(self, *e):
sentence = " ".join(self._sent)
title = "Edit Text"
instr = "Enter a new sentence to parse."
4 years ago
EntryDialog(self._top, sentence, instr, self.set_sentence, title)
def set_sentence(self, sentence):
self._sent = sentence.split() # [XX] use tagged?
self.reset()
def app():
"""
Create a recursive descent parser demo, using a simple grammar and
text.
"""
from nltk.grammar import CFG
grammar = CFG.fromstring(
"""
# Grammatical productions.
S -> NP VP
NP -> Det N PP | Det N
VP -> V NP PP | V NP | V
PP -> P NP
# Lexical productions.
NP -> 'I'
Det -> 'the' | 'a'
N -> 'man' | 'park' | 'dog' | 'telescope'
V -> 'ate' | 'saw'
P -> 'in' | 'under' | 'with'
"""
)
sent = "the dog saw a man in the park".split()
4 years ago
RecursiveDescentApp(grammar, sent).mainloop()
if __name__ == "__main__":
4 years ago
app()
__all__ = ["app"]