Skip to content

Commit 9653e33

Browse files
committed
Port the Perl module's deparse() to Python
1 parent e0408ac commit 9653e33

1 file changed

Lines changed: 262 additions & 0 deletions

File tree

rivescript/rivescript.py

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)