Package nltk :: Package draw :: Module srparser
[hide private]
[frames] | no frames]

Source Code for Module nltk.draw.srparser

  1  # Natural Language Toolkit: Shift/Reduce Parser Demo 
  2  # 
  3  # Copyright (C) 2001-2008 NLTK Project 
  4  # Author: Edward Loper <[email protected]> 
  5  # URL: <http://nltk.org> 
  6  # For license information, see LICENSE.TXT 
  7  # 
  8  # $Id: srparser.py 6265 2008-07-26 09:25:03Z stevenbird $ 
  9   
 10  """ 
 11  A graphical tool for exploring the shift/reduce parser. 
 12   
 13  The shift/reduce parser maintains a stack, which records the structure 
 14  of the portion of the text that has been parsed.  The stack is 
 15  initially empty.  Its contents are shown on the left side of the main 
 16  canvas. 
 17   
 18  On the right side of the main canvas is the remaining text.  This is 
 19  the portion of the text which has not yet been considered by the 
 20  parser. 
 21   
 22  The parser builds up a tree structure for the text using two 
 23  operations:  
 24   
 25    - "shift" moves the first token from the remaining text to the top 
 26      of the stack.  In the demo, the top of the stack is its right-hand 
 27      side. 
 28    - "reduce" uses a grammar production to combine the rightmost stack 
 29      elements into a single tree token. 
 30   
 31  You can control the parser's operation by using the "shift" and 
 32  "reduce" buttons; or you can use the "step" button to let the parser 
 33  automatically decide which operation to apply.  The parser uses the 
 34  following rules to decide which operation to apply: 
 35   
 36    - Only shift if no reductions are available. 
 37    - If multiple reductions are available, then apply the reduction 
 38      whose CFG production is listed earliest in the grammar. 
 39   
 40  The "reduce" button applies the reduction whose CFG production is 
 41  listed earliest in the grammar.  There are two ways to manually choose 
 42  which reduction to apply: 
 43   
 44    - Click on a CFG production from the list of available reductions, 
 45      on the left side of the main window.  The reduction based on that 
 46      production will be applied to the top of the stack. 
 47    - Click on one of the stack elements.  A popup window will appear,  
 48      containing all available reductions.  Select one, and it will be 
 49      applied to the top of the stack. 
 50   
 51  Note that reductions can only be applied to the top of the stack. 
 52   
 53  Keyboard Shortcuts:: 
 54        [Space]\t Perform the next shift or reduce operation 
 55        [s]\t Perform a shift operation 
 56        [r]\t Perform a reduction operation 
 57        [Ctrl-z]\t Undo most recent operation 
 58        [Delete]\t Reset the parser 
 59        [g]\t Show/hide available production list 
 60        [Ctrl-a]\t Toggle animations 
 61        [h]\t Help 
 62        [Ctrl-p]\t Print 
 63        [q]\t Quit 
 64  """ 
 65   
 66  """ 
 67  Possible future improvements: 
 68    - button/window to change and/or select text.  Just pop up a window 
 69      with an entry, and let them modify the text; and then retokenize 
 70      it?  Maybe give a warning if it contains tokens whose types are 
 71      not in the grammar. 
 72    - button/window to change and/or select grammar.  Select from 
 73      several alternative grammars?  Or actually change the grammar?  If 
 74      the later, then I'd want to define nltk.draw.cfg, which would be 
 75      responsible for that. 
 76  """ 
 77   
 78  import string 
 79  from Tkinter import * 
 80  import tkFont 
 81   
 82  from nltk import parse, tokenize 
 83   
 84  from nltk.draw.tree import * 
 85  from nltk.draw import * 
 86  from nltk.draw.cfg import CFGEditor 
 87   
 88           
89 -class ShiftReduceDemo(object):
90 """ 91 A graphical tool for exploring the shift/reduce parser. The tool 92 displays the parser's stack and the remaining text, and allows the 93 user to control the parser's operation. In particular, the user 94 can shift tokens onto the stack, and can perform reductions on the 95 top elements of the stack. A "step" button simply steps through 96 the parsing process, performing the operations that 97 C{parse.ShiftReduceParser} would use. 98 """
99 - def __init__(self, grammar, sent, trace=0):
100 self._sent = sent 101 self._parser = parse.SteppingShiftReduceParser(grammar, trace) 102 103 # Set up the main window. 104 self._top = Tk() 105 self._top.title('Shift Reduce Parser Demo') 106 107 # Animations. animating_lock is a lock to prevent the demo 108 # from performing new operations while it's animating. 109 self._animating_lock = 0 110 self._animate = IntVar(self._top) 111 self._animate.set(10) # = medium 112 113 # The user can hide the grammar. 114 self._show_grammar = IntVar(self._top) 115 self._show_grammar.set(1) 116 117 # Initialize fonts. 118 self._init_fonts(self._top) 119 120 # Set up key bindings. 121 self._init_bindings() 122 123 # Create the basic frames. 124 self._init_menubar(self._top) 125 self._init_buttons(self._top) 126 self._init_feedback(self._top) 127 self._init_grammar(self._top) 128 self._init_canvas(self._top) 129 130 # A popup menu for reducing. 131 self._reduce_menu = Menu(self._canvas, tearoff=0) 132 133 # Reset the demo, and set the feedback frame to empty. 134 self.reset() 135 self._lastoper1['text'] = ''
136 137 ######################################### 138 ## Initialization Helpers 139 ######################################### 140
141 - def _init_fonts(self, root):
142 # See: <http://www.astro.washington.edu/owen/ROTKFolklore.html> 143 self._sysfont = tkFont.Font(font=Button()["font"]) 144 root.option_add("*Font", self._sysfont) 145 146 # TWhat's our font size (default=same as sysfont) 147 self._size = IntVar(root) 148 self._size.set(self._sysfont.cget('size')) 149 150 self._boldfont = tkFont.Font(family='helvetica', weight='bold', 151 size=self._size.get()) 152 self._font = tkFont.Font(family='helvetica', 153 size=self._size.get())
154
155 - def _init_grammar(self, parent):
156 # Grammar view. 157 self._prodframe = listframe = Frame(parent) 158 self._prodframe.pack(fill='both', side='left', padx=2) 159 self._prodlist_label = Label(self._prodframe, 160 font=self._boldfont, 161 text='Available Reductions') 162 self._prodlist_label.pack() 163 self._prodlist = Listbox(self._prodframe, selectmode='single', 164 relief='groove', background='white', 165 foreground='#909090', 166 font=self._font, 167 selectforeground='#004040', 168 selectbackground='#c0f0c0') 169 170 self._prodlist.pack(side='right', fill='both', expand=1) 171 172 self._productions = list(self._parser.grammar().productions()) 173 for production in self._productions: 174 self._prodlist.insert('end', (' %s' % production)) 175 self._prodlist.config(height=min(len(self._productions), 25)) 176 177 # Add a scrollbar if there are more than 25 productions. 178 if 1:#len(self._productions) > 25: 179 listscroll = Scrollbar(self._prodframe, 180 orient='vertical') 181 self._prodlist.config(yscrollcommand = listscroll.set) 182 listscroll.config(command=self._prodlist.yview) 183 listscroll.pack(side='left', fill='y') 184 185 # If they select a production, apply it. 186 self._prodlist.bind('<<ListboxSelect>>', self._prodlist_select) 187 188 # When they hover over a production, highlight it. 189 self._hover = -1 190 self._prodlist.bind('<Motion>', self._highlight_hover) 191 self._prodlist.bind('<Leave>', self._clear_hover)
192
193 - def _init_bindings(self):
194 # Quit 195 self._top.bind('<Control-q>', self.destroy) 196 self._top.bind('<Control-x>', self.destroy) 197 self._top.bind('<Alt-q>', self.destroy) 198 self._top.bind('<Alt-x>', self.destroy) 199 200 # Ops (step, shift, reduce, undo) 201 self._top.bind('<space>', self.step) 202 self._top.bind('<s>', self.shift) 203 self._top.bind('<Alt-s>', self.shift) 204 self._top.bind('<Control-s>', self.shift) 205 self._top.bind('<r>', self.reduce) 206 self._top.bind('<Alt-r>', self.reduce) 207 self._top.bind('<Control-r>', self.reduce) 208 self._top.bind('<Delete>', self.reset) 209 self._top.bind('<u>', self.undo) 210 self._top.bind('<Alt-u>', self.undo) 211 self._top.bind('<Control-u>', self.undo) 212 self._top.bind('<Control-z>', self.undo) 213 self._top.bind('<BackSpace>', self.undo) 214 215 # Misc 216 self._top.bind('<Control-p>', self.postscript) 217 self._top.bind('<Control-h>', self.help) 218 self._top.bind('<F1>', self.help) 219 self._top.bind('<Control-g>', self.edit_grammar) 220 self._top.bind('<Control-t>', self.edit_sentence) 221 222 # Animation speed control 223 self._top.bind('-', lambda e,a=self._animate:a.set(20)) 224 self._top.bind('=', lambda e,a=self._animate:a.set(10)) 225 self._top.bind('+', lambda e,a=self._animate:a.set(4))
226
227 - def _init_buttons(self, parent):
228 # Set up the frames. 229 self._buttonframe = buttonframe = Frame(parent) 230 buttonframe.pack(fill='none', side='bottom') 231 Button(buttonframe, text='Step', 232 background='#90c0d0', foreground='black', 233 command=self.step,).pack(side='left') 234 Button(buttonframe, text='Shift', underline=0, 235 background='#90f090', foreground='black', 236 command=self.shift).pack(side='left') 237 Button(buttonframe, text='Reduce', underline=0, 238 background='#90f090', foreground='black', 239 command=self.reduce).pack(side='left') 240 Button(buttonframe, text='Undo', underline=0, 241 background='#f0a0a0', foreground='black', 242 command=self.undo).pack(side='left')
243
244 - def _init_menubar(self, parent):
245 menubar = Menu(parent) 246 247 filemenu = Menu(menubar, tearoff=0) 248 filemenu.add_command(label='Reset Parser', underline=0, 249 command=self.reset, accelerator='Del') 250 filemenu.add_command(label='Print to Postscript', underline=0, 251 command=self.postscript, accelerator='Ctrl-p') 252 filemenu.add_command(label='Exit', underline=1, 253 command=self.destroy, accelerator='Ctrl-x') 254 menubar.add_cascade(label='File', underline=0, menu=filemenu) 255 256 editmenu = Menu(menubar, tearoff=0) 257 editmenu.add_command(label='Edit Grammar', underline=5, 258 command=self.edit_grammar, 259 accelerator='Ctrl-g') 260 editmenu.add_command(label='Edit Text', underline=5, 261 command=self.edit_sentence, 262 accelerator='Ctrl-t') 263 menubar.add_cascade(label='Edit', underline=0, menu=editmenu) 264 265 rulemenu = Menu(menubar, tearoff=0) 266 rulemenu.add_command(label='Step', underline=1, 267 command=self.step, accelerator='Space') 268 rulemenu.add_separator() 269 rulemenu.add_command(label='Shift', underline=0, 270 command=self.shift, accelerator='Ctrl-s') 271 rulemenu.add_command(label='Reduce', underline=0, 272 command=self.reduce, accelerator='Ctrl-r') 273 rulemenu.add_separator() 274 rulemenu.add_command(label='Undo', underline=0, 275 command=self.undo, accelerator='Ctrl-u') 276 menubar.add_cascade(label='Apply', underline=0, menu=rulemenu) 277 278 viewmenu = Menu(menubar, tearoff=0) 279 viewmenu.add_checkbutton(label="Show Grammar", underline=0, 280 variable=self._show_grammar, 281 command=self._toggle_grammar) 282 viewmenu.add_separator() 283 viewmenu.add_radiobutton(label='Tiny', variable=self._size, 284 underline=0, value=10, command=self.resize) 285 viewmenu.add_radiobutton(label='Small', variable=self._size, 286 underline=0, value=12, command=self.resize) 287 viewmenu.add_radiobutton(label='Medium', variable=self._size, 288 underline=0, value=14, command=self.resize) 289 viewmenu.add_radiobutton(label='Large', variable=self._size, 290 underline=0, value=18, command=self.resize) 291 viewmenu.add_radiobutton(label='Huge', variable=self._size, 292 underline=0, value=24, command=self.resize) 293 menubar.add_cascade(label='View', underline=0, menu=viewmenu) 294 295 animatemenu = Menu(menubar, tearoff=0) 296 animatemenu.add_radiobutton(label="No Animation", underline=0, 297 variable=self._animate, value=0) 298 animatemenu.add_radiobutton(label="Slow Animation", underline=0, 299 variable=self._animate, value=20, 300 accelerator='-') 301 animatemenu.add_radiobutton(label="Normal Animation", underline=0, 302 variable=self._animate, value=10, 303 accelerator='=') 304 animatemenu.add_radiobutton(label="Fast Animation", underline=0, 305 variable=self._animate, value=4, 306 accelerator='+') 307 menubar.add_cascade(label="Animate", underline=1, menu=animatemenu) 308 309 310 helpmenu = Menu(menubar, tearoff=0) 311 helpmenu.add_command(label='About', underline=0, 312 command=self.about) 313 helpmenu.add_command(label='Instructions', underline=0, 314 command=self.help, accelerator='F1') 315 menubar.add_cascade(label='Help', underline=0, menu=helpmenu) 316 317 parent.config(menu=menubar)
318
319 - def _init_feedback(self, parent):
320 self._feedbackframe = feedbackframe = Frame(parent) 321 feedbackframe.pack(fill='x', side='bottom', padx=3, pady=3) 322 self._lastoper_label = Label(feedbackframe, text='Last Operation:', 323 font=self._font) 324 self._lastoper_label.pack(side='left') 325 lastoperframe = Frame(feedbackframe, relief='sunken', border=1) 326 lastoperframe.pack(fill='x', side='right', expand=1, padx=5) 327 self._lastoper1 = Label(lastoperframe, foreground='#007070', 328 background='#f0f0f0', font=self._font) 329 self._lastoper2 = Label(lastoperframe, anchor='w', width=30, 330 foreground='#004040', background='#f0f0f0', 331 font=self._font) 332 self._lastoper1.pack(side='left') 333 self._lastoper2.pack(side='left', fill='x', expand=1)
334
335 - def _init_canvas(self, parent):
336 self._cframe = CanvasFrame(parent, background='white', 337 width=525, closeenough=10, 338 border=2, relief='sunken') 339 self._cframe.pack(expand=1, fill='both', side='top', pady=2) 340 canvas = self._canvas = self._cframe.canvas() 341 342 self._stackwidgets = [] 343 self._rtextwidgets = [] 344 self._titlebar = canvas.create_rectangle(0,0,0,0, fill='#c0f0f0', 345 outline='black') 346 self._exprline = canvas.create_line(0,0,0,0, dash='.') 347 self._stacktop = canvas.create_line(0,0,0,0, fill='#408080') 348 size = self._size.get()+4 349 self._stacklabel = TextWidget(canvas, 'Stack', color='#004040', 350 font=self._boldfont) 351 self._rtextlabel = TextWidget(canvas, 'Remaining Text', 352 color='#004040', font=self._boldfont) 353 self._cframe.add_widget(self._stacklabel) 354 self._cframe.add_widget(self._rtextlabel)
355 356 ######################################### 357 ## Main draw procedure 358 ######################################### 359
360 - def _redraw(self):
361 scrollregion = self._canvas['scrollregion'].split() 362 (cx1, cy1, cx2, cy2) = [int(c) for c in scrollregion] 363 364 # Delete the old stack & rtext widgets. 365 for stackwidget in self._stackwidgets: 366 self._cframe.destroy_widget(stackwidget) 367 self._stackwidgets = [] 368 for rtextwidget in self._rtextwidgets: 369 self._cframe.destroy_widget(rtextwidget) 370 self._rtextwidgets = [] 371 372 # Position the titlebar & exprline 373 (x1, y1, x2, y2) = self._stacklabel.bbox() 374 y = y2-y1+10 375 self._canvas.coords(self._titlebar, -5000, 0, 5000, y-4) 376 self._canvas.coords(self._exprline, 0, y*2-10, 5000, y*2-10) 377 378 # Position the titlebar labels.. 379 (x1, y1, x2, y2) = self._stacklabel.bbox() 380 self._stacklabel.move(5-x1, 3-y1) 381 (x1, y1, x2, y2) = self._rtextlabel.bbox() 382 self._rtextlabel.move(cx2-x2-5, 3-y1) 383 384 # Draw the stack. 385 stackx = 5 386 for tok in self._parser.stack(): 387 if isinstance(tok, parse.Tree): 388 attribs = {'tree_color': '#4080a0', 'tree_width': 2, 389 'node_font': self._boldfont, 390 'node_color': '#006060', 391 'leaf_color': '#006060', 'leaf_font':self._font} 392 widget = tree_to_treesegment(self._canvas, tok, 393 **attribs) 394 widget.node()['color'] = '#000000' 395 else: 396 widget = TextWidget(self._canvas, tok, 397 color='#000000', font=self._font) 398 widget.bind_click(self._popup_reduce) 399 self._stackwidgets.append(widget) 400 self._cframe.add_widget(widget, stackx, y) 401 stackx = widget.bbox()[2] + 10 402 403 # Draw the remaining text. 404 rtextwidth = 0 405 for tok in self._parser.remaining_text(): 406 widget = TextWidget(self._canvas, tok, 407 color='#000000', font=self._font) 408 self._rtextwidgets.append(widget) 409 self._cframe.add_widget(widget, rtextwidth, y) 410 rtextwidth = widget.bbox()[2] + 4 411 412 # Allow enough room to shift the next token (for animations) 413 if len(self._rtextwidgets) > 0: 414 stackx += self._rtextwidgets[0].width() 415 416 # Move the remaining text to the correct location (keep it 417 # right-justified, when possible); and move the remaining text 418 # label, if necessary. 419 stackx = max(stackx, self._stacklabel.width()+25) 420 rlabelwidth = self._rtextlabel.width()+10 421 if stackx >= cx2-max(rtextwidth, rlabelwidth): 422 cx2 = stackx + max(rtextwidth, rlabelwidth) 423 for rtextwidget in self._rtextwidgets: 424 rtextwidget.move(4+cx2-rtextwidth, 0) 425 self._rtextlabel.move(cx2-self._rtextlabel.bbox()[2]-5, 0) 426 427 midx = (stackx + cx2-max(rtextwidth, rlabelwidth))/2 428 self._canvas.coords(self._stacktop, midx, 0, midx, 5000) 429 (x1, y1, x2, y2) = self._stacklabel.bbox() 430 431 # Set up binding to allow them to shift a token by dragging it. 432 if len(self._rtextwidgets) > 0: 433 def drag_shift(widget, midx=midx, self=self): 434 if widget.bbox()[0] < midx: self.shift() 435 else: self._redraw()
436 self._rtextwidgets[0].bind_drag(drag_shift) 437 self._rtextwidgets[0].bind_click(self.shift) 438 439 # Draw the stack top. 440 self._highlight_productions()
441
442 - def _draw_stack_top(self, widget):
443 # hack.. 444 midx = widget.bbox()[2]+50 445 self._canvas.coords(self._stacktop, midx, 0, midx, 5000)
446
447 - def _highlight_productions(self):
448 # Highlight the productions that can be reduced. 449 self._prodlist.selection_clear(0, 'end') 450 for prod in self._parser.reducible_productions(): 451 index = self._productions.index(prod) 452 self._prodlist.selection_set(index)
453 454 ######################################### 455 ## Button Callbacks 456 ######################################### 457
458 - def destroy(self, *e):
459 if self._top is None: return 460 self._top.destroy() 461 self._top = None
462
463 - def reset(self, *e):
464 self._parser.initialize(self._sent) 465 self._lastoper1['text'] = 'Reset Demo' 466 self._lastoper2['text'] = '' 467 self._redraw()
468
469 - def step(self, *e):
470 if self.reduce(): return 1 471 elif self.shift(): return 1 472 else: 473 if len(self._parser.parses()) > 0: 474 self._lastoper1['text'] = 'Finished:' 475 self._lastoper2['text'] = 'Success' 476 else: 477 self._lastoper1['text'] = 'Finished:' 478 self._lastoper2['text'] = 'Failure'
479
480 - def shift(self, *e):
481 if self._animating_lock: return 482 if self._parser.shift(): 483 tok = self._parser.stack()[-1] 484 self._lastoper1['text'] = 'Shift:' 485 self._lastoper2['text'] = '%r' % tok 486 if self._animate.get(): 487 self._animate_shift() 488 else: 489 self._redraw() 490 return 1 491 return 0
492
493 - def reduce(self, *e):
494 if self._animating_lock: return 495 production = self._parser.reduce() 496 if production: 497 self._lastoper1['text'] = 'Reduce:' 498 self._lastoper2['text'] = '%s' % production 499 if self._animate.get(): 500 self._animate_reduce() 501 else: 502 self._redraw() 503 return production
504
505 - def undo(self, *e):
506 if self._animating_lock: return 507 if self._parser.undo(): 508 self._redraw()
509
510 - def postscript(self, *e):
511 self._cframe.print_to_file()
512
513 - def mainloop(self, *args, **kwargs):
514 """ 515 Enter the Tkinter mainloop. This function must be called if 516 this demo is created from a non-interactive program (e.g. 517 from a secript); otherwise, the demo will close as soon as 518 the script completes. 519 """ 520 if in_idle(): return 521 self._top.mainloop(*args, **kwargs)
522 523 ######################################### 524 ## Menubar callbacks 525 ######################################### 526
527 - def resize(self, size=None):
528 if size is not None: self._size.set(size) 529 size = self._size.get() 530 self._font.configure(size=-(abs(size))) 531 self._boldfont.configure(size=-(abs(size))) 532 self._sysfont.configure(size=-(abs(size))) 533 534 #self._stacklabel['font'] = ('helvetica', -size-4, 'bold') 535 #self._rtextlabel['font'] = ('helvetica', -size-4, 'bold') 536 #self._lastoper_label['font'] = ('helvetica', -size) 537 #self._lastoper1['font'] = ('helvetica', -size) 538 #self._lastoper2['font'] = ('helvetica', -size) 539 #self._prodlist['font'] = ('helvetica', -size) 540 #self._prodlist_label['font'] = ('helvetica', -size-2, 'bold') 541 self._redraw()
542
543 - def help(self, *e):
544 # The default font's not very legible; try using 'fixed' instead. 545 try: 546 ShowText(self._top, 'Help: Chart Parser Demo', 547 (__doc__).strip(), width=75, font='fixed') 548 except: 549 ShowText(self._top, 'Help: Chart Parser Demo', 550 (__doc__).strip(), width=75)
551
552 - def about(self, *e):
553 ABOUT = ("NLTK Shift-Reduce Parser Demo\n"+ 554 "Written by Edward Loper") 555 TITLE = 'About: Shift-Reduce Parser Demo' 556 try: 557 from tkMessageBox import Message 558 Message(message=ABOUT, title=TITLE).show() 559 except: 560 ShowText(self._top, TITLE, ABOUT)
561
562 - def edit_grammar(self, *e):
563 CFGEditor(self._top, self._parser.grammar(), self.set_grammar)
564
565 - def set_grammar(self, grammar):
566 self._parser.set_grammar(grammar) 567 self._productions = list(grammar.productions()) 568 self._prodlist.delete(0, 'end') 569 for production in self._productions: 570 self._prodlist.insert('end', (' %s' % production))
571
572 - def edit_sentence(self, *e):
573 sentence = string.join(self._sent) 574 title = 'Edit Text' 575 instr = 'Enter a new sentence to parse.' 576 EntryDialog(self._top, sentence, instr, self.set_sentence, title)
577
578 - def set_sentence(self, sentence):
579 self._sent = sent.split() #[XX] use tagged? 580 self.reset()
581 582 ######################################### 583 ## Reduce Production Selection 584 ######################################### 585
586 - def _toggle_grammar(self, *e):
587 if self._show_grammar.get(): 588 self._prodframe.pack(fill='both', side='left', padx=2, 589 after=self._feedbackframe) 590 self._lastoper1['text'] = 'Show Grammar' 591 else: 592 self._prodframe.pack_forget() 593 self._lastoper1['text'] = 'Hide Grammar' 594 self._lastoper2['text'] = ''
595
596 - def _prodlist_select(self, event):
597 selection = self._prodlist.curselection() 598 if len(selection) != 1: return 599 index = int(selection[0]) 600 production = self._parser.reduce(self._productions[index]) 601 if production: 602 self._lastoper1['text'] = 'Reduce:' 603 self._lastoper2['text'] = '%s' % production 604 if self._animate.get(): 605 self._animate_reduce() 606 else: 607 self._redraw() 608 else: 609 # Reset the production selections. 610 self._prodlist.selection_clear(0, 'end') 611 for prod in self._parser.reducible_productions(): 612 index = self._productions.index(prod) 613 self._prodlist.selection_set(index)
614
615 - def _popup_reduce(self, widget):
616 # Remove old commands. 617 productions = self._parser.reducible_productions() 618 if len(productions) == 0: return 619 620 self._reduce_menu.delete(0, 'end') 621 for production in productions: 622 self._reduce_menu.add_command(label=str(production), 623 command=self.reduce) 624 self._reduce_menu.post(self._canvas.winfo_pointerx(), 625 self._canvas.winfo_pointery())
626 627 ######################################### 628 ## Animations 629 ######################################### 630
631 - def _animate_shift(self):
632 # What widget are we shifting? 633 widget = self._rtextwidgets[0] 634 635 # Where are we shifting from & to? 636 right = widget.bbox()[0] 637 if len(self._stackwidgets) == 0: left = 5 638 else: left = self._stackwidgets[-1].bbox()[2]+10 639 640 # Start animating. 641 dt = self._animate.get() 642 dx = (left-right)*1.0/dt 643 self._animate_shift_frame(dt, widget, dx)
644
645 - def _animate_shift_frame(self, frame, widget, dx):
646 if frame > 0: 647 self._animating_lock = 1 648 widget.move(dx, 0) 649 self._top.after(10, self._animate_shift_frame, 650 frame-1, widget, dx) 651 else: 652 # but: stacktop?? 653 654 # Shift the widget to the stack. 655 del self._rtextwidgets[0] 656 self._stackwidgets.append(widget) 657 self._animating_lock = 0 658 659 # Display the available productions. 660 self._draw_stack_top(widget) 661 self._highlight_productions()
662
663 - def _animate_reduce(self):
664 # What widgets are we shifting? 665 numwidgets = len(self._parser.stack()[-1]) # number of children 666 widgets = self._stackwidgets[-numwidgets:] 667 668 # How far are we moving? 669 if isinstance(widgets[0], TreeSegmentWidget): 670 ydist = 15 + widgets[0].node().height() 671 else: 672 ydist = 15 + widgets[0].height() 673 674 # Start animating. 675 dt = self._animate.get() 676 dy = ydist*2.0/dt 677 self._animate_reduce_frame(dt/2, widgets, dy)
678
679 - def _animate_reduce_frame(self, frame, widgets, dy):
680 if frame > 0: 681 self._animating_lock = 1 682 for widget in widgets: widget.move(0, dy) 683 self._top.after(10, self._animate_reduce_frame, 684 frame-1, widgets, dy) 685 else: 686 del self._stackwidgets[-len(widgets):] 687 for widget in widgets: 688 self._cframe.remove_widget(widget) 689 tok = self._parser.stack()[-1] 690 if not isinstance(tok, parse.Tree): raise ValueError() 691 label = TextWidget(self._canvas, str(tok.node), color='#006060', 692 font=self._boldfont) 693 widget = TreeSegmentWidget(self._canvas, label, widgets, 694 width=2) 695 (x1, y1, x2, y2) = self._stacklabel.bbox() 696 y = y2-y1+10 697 if not self._stackwidgets: x = 5 698 else: x = self._stackwidgets[-1].bbox()[2] + 10 699 self._cframe.add_widget(widget, x, y) 700 self._stackwidgets.append(widget) 701 702 # Display the available productions. 703 self._draw_stack_top(widget) 704 self._highlight_productions() 705 706 # # Delete the old widgets.. 707 # del self._stackwidgets[-len(widgets):] 708 # for widget in widgets: 709 # self._cframe.destroy_widget(widget) 710 # 711 # # Make a new one. 712 # tok = self._parser.stack()[-1] 713 # if isinstance(tok, parse.Tree): 714 # attribs = {'tree_color': '#4080a0', 'tree_width': 2, 715 # 'node_font': bold, 'node_color': '#006060', 716 # 'leaf_color': '#006060', 'leaf_font':self._font} 717 # widget = tree_to_treesegment(self._canvas, tok.type(), 718 # **attribs) 719 # widget.node()['color'] = '#000000' 720 # else: 721 # widget = TextWidget(self._canvas, tok.type(), 722 # color='#000000', font=self._font) 723 # widget.bind_click(self._popup_reduce) 724 # (x1, y1, x2, y2) = self._stacklabel.bbox() 725 # y = y2-y1+10 726 # if not self._stackwidgets: x = 5 727 # else: x = self._stackwidgets[-1].bbox()[2] + 10 728 # self._cframe.add_widget(widget, x, y) 729 # self._stackwidgets.append(widget) 730 731 #self._redraw() 732 self._animating_lock = 0
733 734 ######################################### 735 ## Hovering. 736 ######################################### 737
738 - def _highlight_hover(self, event):
739 # What production are we hovering over? 740 index = self._prodlist.nearest(event.y) 741 if self._hover == index: return 742 743 # Clear any previous hover highlighting. 744 self._clear_hover() 745 746 # If the production corresponds to an available reduction, 747 # highlight the stack. 748 selection = [int(s) for s in self._prodlist.curselection()] 749 if index in selection: 750 rhslen = len(self._productions[index].rhs()) 751 for stackwidget in self._stackwidgets[-rhslen:]: 752 if isinstance(stackwidget, TreeSegmentWidget): 753 stackwidget.node()['color'] = '#00a000' 754 else: 755 stackwidget['color'] = '#00a000' 756 757 # Remember what production we're hovering over. 758 self._hover = index
759
760 - def _clear_hover(self, *event):
761 # Clear any previous hover highlighting. 762 if self._hover == -1: return 763 self._hover = -1 764 for stackwidget in self._stackwidgets: 765 if isinstance(stackwidget, TreeSegmentWidget): 766 stackwidget.node()['color'] = 'black' 767 else: 768 stackwidget['color'] = 'black'
769 770
771 -def demo():
772 """ 773 Create a shift reduce parser demo, using a simple grammar and 774 text. 775 """ 776 777 from nltk import cfg 778 nonterminals = 'S VP NP PP P N Name V Det' 779 (S, VP, NP, PP, P, N, Name, V, Det) = [cfg.Nonterminal(s) 780 for s in nonterminals.split()] 781 782 productions = ( 783 # Syntactic Productions 784 cfg.Production(S, [NP, VP]), 785 cfg.Production(NP, [Det, N]), 786 cfg.Production(NP, [NP, PP]), 787 cfg.Production(VP, [VP, PP]), 788 cfg.Production(VP, [V, NP, PP]), 789 cfg.Production(VP, [V, NP]), 790 cfg.Production(PP, [P, NP]), 791 792 # Lexical Productions 793 cfg.Production(NP, ['I']), cfg.Production(Det, ['the']), 794 cfg.Production(Det, ['a']), cfg.Production(N, ['man']), 795 cfg.Production(V, ['saw']), cfg.Production(P, ['in']), 796 cfg.Production(P, ['with']), cfg.Production(N, ['park']), 797 cfg.Production(N, ['dog']), cfg.Production(N, ['statue']), 798 cfg.Production(Det, ['my']), 799 ) 800 801 grammar = cfg.Grammar(S, productions) 802 803 # tokenize the sentence 804 sent = 'my dog saw a man in the park with a statue'.split() 805 806 ShiftReduceDemo(grammar, sent).mainloop()
807 808 if __name__ == '__main__': demo() 809