@@ -640,6 +640,268 @@ def check_syntax(self, cmd, line):
640640
641641 return None
642642
643+ def deparse (self ):
644+ """Return the in-memory RiveScript document as a Python data structure.
645+
646+ This would be useful for developing a user interface for editing
647+ RiveScript replies without having to edit the RiveScript code
648+ manually."""
649+
650+ # Data to return.
651+ result = {
652+ "begin" : {
653+ "global" : {},
654+ "var" : {},
655+ "sub" : {},
656+ "person" : {},
657+ "array" : {},
658+ "triggers" : {},
659+ "that" : {},
660+ },
661+ "topic" : {},
662+ "that" : {},
663+ "inherit" : {},
664+ "include" : {},
665+ }
666+
667+ # Populate the config fields.
668+ if self ._debug :
669+ result ["begin" ]["global" ]["debug" ] = self ._debug
670+ if self ._depth != 50 :
671+ result ["begin" ]["global" ]["depth" ] = 50
672+
673+ # Definitions
674+ result ["begin" ]["var" ] = self ._bvars .copy ()
675+ result ["begin" ]["sub" ] = self ._subs .copy ()
676+ result ["begin" ]["person" ] = self ._person .copy ()
677+ result ["begin" ]["array" ] = self ._arrays .copy ()
678+ result ["begin" ]["global" ].update (self ._gvars .copy ())
679+
680+ # Topic Triggers.
681+ for topic in self ._topics :
682+ dest = {} # Where to place the topic info
683+
684+ if topic == "__begin__" :
685+ # Begin block.
686+ dest = result ["begin" ]["triggers" ]
687+ else :
688+ # Normal topic.
689+ if not topic in result ["topic" ]:
690+ result ["topic" ][topic ] = {}
691+ dest = result ["topic" ][topic ]
692+
693+ # Copy the triggers.
694+ for trig , data in self ._topics [topic ].iteritems ():
695+ dest [trig ] = self ._copy_trigger (trig , data )
696+
697+ # %Previous's.
698+ for topic in self ._thats :
699+ dest = {} # Where to place the topic info
700+
701+ if topic == "__begin__" :
702+ # Begin block.
703+ dest = result ["begin" ]["that" ]
704+ else :
705+ # Normal topic.
706+ if not topic in result ["that" ]:
707+ result ["that" ][topic ] = {}
708+ dest = result ["that" ][topic ]
709+
710+ # The "that" structure is backwards: bot reply, then trigger, then info.
711+ for previous , pdata in self ._thats [topic ].iteritems ():
712+ for trig , data in pdata .iteritems ():
713+ dest [trig ] = self ._copy_trigger (trig , data , previous )
714+
715+ # Inherits/Includes.
716+ for topic , data in self ._lineage .iteritems ():
717+ result ["inherit" ][topic ] = []
718+ for inherit in data :
719+ result ["inherit" ][topic ].append (inherit )
720+ for topic , data in self ._includes .iteritems ():
721+ result ["include" ][topic ] = []
722+ for include in data :
723+ result ["include" ][topic ].append (include )
724+
725+ return result
726+
727+ def write (self , fh , deparsed = None ):
728+ """Write the currently parsed RiveScript data into a file.
729+
730+ Pass either a file name (string) or a file handle object.
731+
732+ This uses `deparse()` to dump a representation of the loaded data and
733+ writes it to the destination file. If you provide your own data as the
734+ `deparsed` argument, it will use that data instead of calling
735+ `deparse()` itself. This way you can use `deparse()`, edit the data,
736+ and use that to write the RiveScript document (for example, to be used
737+ by a user interface for editing RiveScript without writing the code
738+ directly)."""
739+
740+ # Passed a string instead of a file handle?
741+ if type (fh ) is str :
742+ fh = codecs .open (fh , "w" , "utf-8" )
743+
744+ # Deparse the loaded data.
745+ if deparsed is None :
746+ deparsed = self .deparse ()
747+
748+ # Start at the beginning.
749+ fh .write ("// Written by rivescript.deparse()\n " )
750+ fh .write ("! version = 2.0\n \n " )
751+
752+ # Variables of all sorts!
753+ for kind in ["global" , "var" , "sub" , "person" , "array" ]:
754+ if len (deparsed ["begin" ][kind ].keys ()) == 0 :
755+ continue
756+
757+ for var in sorted (deparsed ["begin" ][kind ].keys ()):
758+ # Array types need to be separated by either spaces or pipes.
759+ data = deparsed ["begin" ][kind ][var ]
760+ if type (data ) not in [str , unicode ]:
761+ needs_pipes = False
762+ for test in data :
763+ if " " in test :
764+ needs_pipes = True
765+ break
766+
767+ # Word-wrap the result, target width is 78 chars minus the
768+ # kind, var, and spaces and equals sign.
769+ width = 78 - len (kind ) - len (var ) - 4
770+
771+ if needs_pipes :
772+ data = self ._write_wrapped ("|" .join (data ), sep = "|" )
773+ else :
774+ data = " " .join (data )
775+
776+ fh .write ("! {kind} {var} = {data}\n " .format (
777+ kind = kind ,
778+ var = var ,
779+ data = data ,
780+ ))
781+ fh .write ("\n " )
782+
783+ # Begin block.
784+ if len (deparsed ["begin" ]["triggers" ].keys ()):
785+ fh .write ("> begin\n \n " )
786+ self ._write_triggers (fh , deparsed ["begin" ]["triggers" ], indent = "\t " )
787+ fh .write ("< begin\n \n " )
788+
789+ # The topics. Random first!
790+ topics = ["random" ]
791+ topics .extend (sorted (deparsed ["topic" ].keys ()))
792+ done_random = False
793+ for topic in topics :
794+ if not topic in deparsed ["topic" ]: continue
795+ if topic == "random" and done_random : continue
796+ if topic == "random" : done_random = True
797+
798+ tagged = False # Used > topic tag
799+
800+ if topic != "random" or topic in deparsed ["include" ] or topic in deparsed ["inherit" ]:
801+ tagged = True
802+ fh .write ("> topic " + topic )
803+
804+ if topic in deparsed ["inherit" ]:
805+ fh .write (" inherits " + " " .join (deparsed ["inherit" ][topic ]))
806+ if topic in deparsed ["include" ]:
807+ fh .write (" includes " + " " .join (deparsed ["include" ][topic ]))
808+
809+ fh .write ("\n \n " )
810+
811+ indent = "\t " if tagged else ""
812+ self ._write_triggers (fh , deparsed ["topic" ][topic ], indent = indent )
813+
814+ # Any %Previous's?
815+ if topic in deparsed ["that" ]:
816+ self ._write_triggers (fh , deparsed ["that" ][topic ], indent = indent )
817+
818+ if tagged :
819+ fh .write ("< topic\n \n " )
820+
821+ return True
822+
823+ def _copy_trigger (self , trig , data , previous = None ):
824+ """Make copies of all data below a trigger."""
825+ # Copied data.
826+ dest = {}
827+
828+ if previous :
829+ dest ["previous" ] = previous
830+
831+ if "redirect" in data and data ["redirect" ]:
832+ # @Redirect
833+ dest ["redirect" ] = data ["redirect" ]
834+
835+ if "condition" in data and len (data ["condition" ].keys ()):
836+ # *Condition
837+ dest ["condition" ] = []
838+ for i in sorted (data ["condition" ].keys ()):
839+ dest ["condition" ].append (data ["condition" ][i ])
840+
841+ if "reply" in data and len (data ["reply" ].keys ()):
842+ # -Reply
843+ dest ["reply" ] = []
844+ for i in sorted (data ["reply" ].keys ()):
845+ dest ["reply" ].append (data ["reply" ][i ])
846+
847+ return dest
848+
849+ def _write_triggers (self , fh , triggers , indent = "" ):
850+ """Write triggers to a file handle."""
851+
852+ for trig in sorted (triggers .keys ()):
853+ fh .write (indent + "+ " + self ._write_wrapped (trig , indent = indent ) + "\n " )
854+ d = triggers [trig ]
855+
856+ if "previous" in d :
857+ fh .write (indent + "% " + self ._write_wrapped (d ["previous" ], indent = indent ) + "\n " )
858+
859+ if "condition" in d :
860+ for cond in d ["condition" ]:
861+ fh .write (indent + "* " + self ._write_wrapped (cond , indent = indent ) + "\n " )
862+
863+ if "redirect" in d :
864+ fh .write (indent + "@ " + self ._write_wrapped (d ["redirect" ], indent = indent ) + "\n " )
865+
866+ if "reply" in d :
867+ for reply in d ["reply" ]:
868+ fh .write (indent + "- " + self ._write_wrapped (reply , indent = indent ) + "\n " )
869+
870+ fh .write ("\n " )
871+
872+ def _write_wrapped (self , line , sep = " " , indent = "" , width = 78 ):
873+ """Word-wrap a line of RiveScript code for being written to a file."""
874+
875+ words = line .split (sep )
876+ lines = []
877+ line = ""
878+ buf = []
879+
880+ while len (words ):
881+ buf .append (words .pop (0 ))
882+ line = sep .join (buf )
883+ if len (line ) > width :
884+ # Need to word wrap!
885+ words .insert (0 , buf .pop ()) # Undo
886+ lines .append (sep .join (buf ))
887+ buf = []
888+ line = ""
889+
890+ # Straggler?
891+ if line :
892+ lines .append (line )
893+
894+ # Returned output
895+ result = lines .pop (0 )
896+ if len (lines ):
897+ eol = ""
898+ if sep == " " :
899+ eol = "\s"
900+ for item in lines :
901+ result += eol + "\n " + indent + "^ " + item
902+
903+ return result
904+
643905 def _initTT (self , toplevel , topic , trigger , what = '' ):
644906 """Initialize a Topic Tree data structure."""
645907 if toplevel == 'topics' :
0 commit comments