srparser_app.py 33 KB

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