From 2dd94cb2bdda87053f0911427491321cbbcd900d Mon Sep 17 00:00:00 2001
From: Benoit Favre <benoit.favre@lif.univ-mrs.fr>
Date: Mon, 2 Mar 2015 18:18:22 +0100
Subject: [PATCH] add editor for xml

---
 .options.txt |   2 +-
 command.py   |  63 +++++++++++++++++++++++++++
 validate.py  | 119 +++++++++++++++++++++++++++++++++++++++++++++++++++
 xmledit.py   | 114 ++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 297 insertions(+), 1 deletion(-)
 create mode 100644 command.py
 create mode 100644 validate.py
 create mode 100644 xmledit.py

diff --git a/.options.txt b/.options.txt
index c229903..acb2937 100644
--- a/.options.txt
+++ b/.options.txt
@@ -2,4 +2,4 @@ slu: 0
 xml_filename: data/homeostasis_25nov.xml
 osc_host: 127.0.0.1
 osc_port: 1234
-asr_model: asr/mika-fred-1.cfg
+asr_model: asr/custom.cfg
diff --git a/command.py b/command.py
new file mode 100644
index 0000000..c7f0977
--- /dev/null
+++ b/command.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env python2
+
+import os, threading, locale, sys
+
+from gi.repository import GObject, Gtk, Gdk
+
+GObject.threads_init()
+Gdk.threads_init()
+
+encoding = locale.getpreferredencoding()
+utf8conv = lambda x : unicode(x, encoding).encode('utf8')
+
+class Command(Gtk.ScrolledWindow):
+    def __init__(self):
+        super(Command, self).__init__()
+        self.view = Gtk.TextView()
+        self.view.set_editable(False)
+        self.view.set_cursor_visible(False)
+        self.buffer = self.view.get_buffer()
+        self.add(self.view)
+        self.listeners = []
+
+    def clear(self):
+        self.buffer.set_text('')
+
+    def add_listener(self, listener):
+        self.listeners.append(listener)
+
+    def run(self, command):
+        thread = threading.Thread(target=self._read_command_output, args=(command,))
+        thread.start()
+
+    def _read_command_output(self, command):
+        stdin, stdouterr = os.popen4(command)
+        while 1:
+            line = stdouterr.readline()
+            if not line:
+                break
+            Gdk.threads_enter()
+            self.add_text(utf8conv(line))
+            Gdk.threads_leave()
+        for listener in self.listeners:
+            listener(command, self.buffer.get_text())
+
+    def add_text(self, line):
+        if not line.endswith('\n'):
+            line += '\n'
+        self.buffer.insert_at_cursor(line)
+        adj = self.get_vadjustment()
+        adj.set_value(adj.get_upper() - adj.get_page_size())
+
+if __name__ == '__main__':
+    import signal
+    signal.signal(signal.SIGINT, signal.SIG_DFL)
+
+    command = Command(' '.join(sys.argv[1:]))
+
+    window = Gtk.Window()
+    window.add(command)
+    window.set_default_size(800, 600)
+    window.show_all()
+    window.connect("delete-event", Gtk.main_quit)
+    Gtk.main()
diff --git a/validate.py b/validate.py
new file mode 100644
index 0000000..d74f938
--- /dev/null
+++ b/validate.py
@@ -0,0 +1,119 @@
+from xml.etree import ElementTree as ET
+import re
+
+seen_section_ids = {}
+seen_actions = {}
+
+class VerifyException(Exception):
+    def __init__(self, message, node):
+        self.message = message
+        self.node = node
+    def __str__(self):
+        return self.message + ': ' + ET.tostring(self.node)[:200].strip()
+    def __repr__(self):
+        return str(self)
+
+def is_int(text):
+    return re.match(r'^\d+$', text)
+
+def has_blank(text):
+    return re.match(r'\s', text)
+
+def verify_keyword(node):
+    global seen_actions
+    for key in node.attrib:
+        if key not in ['action', 'lang']:
+            raise VerifyException('attribute "%s" not allowed in <%s>' % (key, node.tag), node)
+    for key in ['action']:
+        if key not in node.attrib:
+            raise VerifyException('node <%s> must contain attribute "%s"' % (node.tag, key), node)
+    if node.get('action').strip() == '':
+        print 'WARNING: empty action for %s' % ET.tostring(node).strip()
+        #raise VerifyException('empty action', node)
+    if has_blank(node.get('action')):
+        raise VerifyException('spaces not allowed in action "%s"', node)
+    seen_actions[node.get('action')] = True
+    if node.get('lang') not in [None, 'eng', 'esp']:
+        raise VerifyException('unsupported lang "%s"' % node.get('lang'), node)
+    for child in node:
+        raise VerifyException('child <%s> not allowed in <%s>' % (child.tag, node.tag), node)
+
+def verify_sequence(node):
+    for key in node.attrib:
+        if key not in ['ordre', 'repetition', 'action', 'lang']:
+            raise VerifyException('attribute "%s" not allowed in <%s>' % (key, node.tag), node)
+    if node.get('lang') not in [None, 'eng', 'esp']:
+        raise VerifyException('unsupported lang "%s"' % node.get('lang'), node)
+    for child in node:
+        if child.tag == 'keyword':
+            verify_keyword(child)
+        else:
+            raise VerifyException('child <%s> not allowed in <%s>' % (child.tag, node.tag), node)
+
+def verify_section(node):
+    global seen_section_ids
+    for key in node.attrib:
+        if key not in ['id', 'action']:
+            raise VerifyException('attribute "%s" not allowed in <%s>' % (key, node.tag), node)
+    for key in ['id']:
+        if key not in node.attrib:
+            raise VerifyException('node <%s> must contain attribute "%s"' % (node.tag, key), node)
+    if not is_int(node.get('id')):
+        raise VerifyException('only integers allowed for section id "%s"' % node.get('id'))
+    if node.get('id') in seen_section_ids:
+        raise VerifyException('repeated section id "%s"' % node.get('id'))
+    seen_section_ids[node.get('id')] = True
+    for child in node:
+        if child.tag == 'sequence':
+            verify_sequence(child)
+        else:
+            raise VerifyException('child <%s> not allowed in <%s>' % (child.tag, node.tag), node)
+    if node.text != None and node.text.strip() != '':
+        raise VerifyException('no text allowed directly in <%s>' % (node.tag), node)
+    if node.tail != None and node.tail.strip() != '':
+        raise VerifyException('no text allowed directly after <%s>' % (node.tag), node)
+
+def verify_liste_section(node):
+    for key in node.attrib:
+        if key not in ['sequences', 'ordre', 'repetition', 'action']:
+            raise VerifyException('attribute "%s" not allowed in <%s>' % (key, node.tag), node)
+    for child in node:
+        if child.tag == 'section':
+            verify_section(child)
+        else:
+            raise VerifyException('child <%s> not allowed in <%s>' % (child.tag, node.tag), node)
+    if node.text != None and node.text.strip() != '':
+        raise VerifyException('no text allowed directly in <%s>' % (node.tag), node)
+    if node.tail != None and node.tail.strip() != '':
+        raise VerifyException('no text allowed directly after <%s>' % (node.tag), node)
+
+def verify_root(node):
+    if node.tag != 'homeostasis':
+        raise VerifyException('root tag should be <homeostasis>')
+    for key in node.attrib:
+        if key not in ['version']:
+            raise VerifyException('attribute "%s" not allowed in <%s>' % (key, node.tag), node)
+    for child in node:
+        if child.tag == 'liste_section':
+            verify_liste_section(child)
+        else:
+            raise VerifyException('child <%s> not allowed in <%s>' % (child.tag, node.tag), node)
+    if node.text != None and node.text.strip() != '':
+        raise VerifyException('no text allowed directly in <%s>' % (node.tag), node)
+    if node.tail != None and node.tail.strip() != '':
+        raise VerifyException('no text allowed directly after <%s>' % (node.tag), node)
+
+def validate_xml(filename):
+    global seen_section_ids, seen_actions
+    seen_section_ids = {}
+    seen_actions = {}
+    try:
+        root = ET.parse(filename).getroot()
+        verify_root(root)
+    except Exception as e:
+        return (False, str(e))
+    return (True, 'successfuly validated "%s"\nfound %d sections, %d types of action' % (filename, len(seen_section_ids), len(seen_actions)))
+
+if __name__ == '__main__':
+    import sys
+    print validate_xml(sys.argv[1])
diff --git a/xmledit.py b/xmledit.py
new file mode 100644
index 0000000..b35b996
--- /dev/null
+++ b/xmledit.py
@@ -0,0 +1,114 @@
+#!/usr/bin/env python2
+
+import signal
+signal.signal(signal.SIGINT, signal.SIG_DFL)
+
+import os, sys
+os.environ['GTK_THEME'] = 'light'
+
+from gi.repository import GObject, Gst, Gtk, Gdk, GtkSource
+
+GObject.threads_init()
+Gdk.threads_init()
+
+import command, validate
+
+class SourceView(Gtk.Window):
+    def __init__(self, filename):
+        super(SourceView, self).__init__()
+
+        self.filename = filename
+
+        self.connect("destroy", self.quit)
+        self.set_default_size(800,600)
+        self.set_border_width(10)
+        self.set_title('SourceView')
+
+        tabs = Gtk.Notebook()
+        self.tabs = tabs
+
+        vbox = Gtk.VBox() # contains tabs and buttons
+
+        self.source_buffer = GtkSource.Buffer()
+        lang_manager = GtkSource.LanguageManager()
+        self.source_buffer.set_language(lang_manager.get_language('xml'))
+        self.source_view = GtkSource.View.new_with_buffer(self.source_buffer)
+        self.source_view.set_show_line_numbers(True)
+        self.source_view.set_tab_width(4)
+        self.source_view.set_wrap_mode(Gtk.WrapMode.WORD)
+        self.scrolled = Gtk.ScrolledWindow()
+        self.scrolled.add(self.source_view)
+        #vbox.pack_start(self.scrolled, True, True, 5)
+
+        self.command = command.Command()
+        #vbox.pack_start(self.command, False, True, 5)
+
+        hbox = Gtk.HBox()
+        revert_button = Gtk.Button('Revert')
+        revert_button.connect('clicked', self.load)
+        hbox.pack_start(revert_button, False, False, 5)
+
+        validate_button = Gtk.Button('Validate')
+        validate_button.connect('clicked', self.validate)
+        hbox.pack_start(validate_button, False, False, 5)
+
+        compile_button = Gtk.Button('Compile')
+        compile_button.connect('clicked', self.compile)
+        hbox.pack_start(compile_button, False, False, 5)
+
+        save_button = Gtk.Button('Save')
+        save_button.connect('clicked', self.save)
+        hbox.pack_start(save_button, False, False, 5)
+
+        vbox.pack_start(tabs, True, True, 5)
+        vbox.pack_start(hbox, False, True, 5)
+
+        tabs.append_page(self.scrolled, Gtk.Label(filename))
+        tabs.append_page(self.command, Gtk.Label('Compilation result'))
+        tabs.set_show_border(False)
+
+        self.add(vbox)
+        self.show_all()
+
+        self.load()
+
+    def load(self, *args):
+        self.source_buffer.set_text(open(self.filename).read())
+
+    def save(self, *args):
+        self.command.clear()
+        try:
+            with open('data/edited.xml', 'w') as fp:
+                bounds = self.source_buffer.get_bounds()
+                fp.write(self.source_buffer.get_text(bounds[0], bounds[1], True))
+        except Exception as e:
+            print e
+            self.command.add_text('FAILED to save as data/edited.xml')
+
+    def validate(self, *args):
+        self.command.clear()
+        self.save()
+        self.command.add_text('VALIDATE...')
+        self.tabs.set_current_page(1)
+        result, message = validate.validate_xml('data/edited.xml')
+        self.command.add_text(message)
+        if result:
+            self.command.add_text('SUCCESS')
+        else:
+            self.command.add_text('FAILED')
+        return result
+
+    def compile(self, *args):
+        self.command.clear()
+        self.tabs.set_current_page(1)
+        self.save()
+        self.validate()
+        self.command.add_text('COMPILE...')
+        self.command.run('./tools/compile.ben.sh data/edited.xml tools/model')
+
+    def quit(self, window):
+        Gtk.main_quit()
+
+if __name__ == '__main__':
+    app = SourceView(sys.argv[1])
+    Gtk.main()
-- 
GitLab