# Copyright (c) 2000-2004 LOGILAB S.A. (Paris, FRANCE). # http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation; either version 2 of the License, or (at your option) any later # version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Software Foundation, Inc., # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. """XmlEditor module""" __revision__ = "$Id: XmlEditor.py,v 1.15 2004/08/06 12:50:41 syt Exp $" from cStringIO import StringIO from gtk import * from xml.parsers.xmlproc.dtdparser import DTDParser from xml.parsers.xmlproc.xmldtd import CompleteDTD from xml.dom.ext import ReleaseNode, StripXml, Print, PrettyPrint from xml.dom.ext.reader import Sax2 try: from xml.dom import EMPTY_NAMESPACE as NO_NS except: # EMPTY_NAMESPACE is not yet defined, use old pyxml version where # the empty namespace is the empty string instead of None NO_NS = '' try: from Ft.Xml.Xpath import Evaluate except: from xml.xpath import Evaluate from logilab.xmltools.XmlTree import XmlTree, utf8_to_gtk def parse_dtd_file(dtd_file, dtd_obj=None): parser = DTDParser() dtd = dtd_obj or CompleteDTD(parser) parser.set_dtd_consumer(dtd) parser.set_dtd_object(dtd) parser.parse_resource(dtd_file) parser.deref() return dtd def prefix_to_nsuri(node): map = {} ns_nodes = Evaluate('namespace::node()',node) for ns in ns_nodes: map[ns.localName]=ns.value return map def get_prefix(qname): splitted = qname.split(':',1) if len(splitted) == 1: return None else: return splitted[0] def get_uri(node ,prefix): map = prefix_to_nsuri(node) return map.get(prefix) def getElementsName(child, dtd, list=None): """ A recursive function that permits to extract allowed elements name from the complex output tuple of ElementType.get_content_model (something like (',', [('caption', '?'), ('|', [('col', '*'), ('colgroup', '*')], ''), ('thead', '?'), ('tfoot', '?'), ('|', [('tbody', '+'), ('tr', '+')], '')], '') : example of the allowed elements of the HTML tag ) Inputs the complex tuple to be processed. Inputs the dtd object from which the elements have been read Inputs the list in which will be stored the elements name Returns the list """ templist = list or [] # processes the case of child == None (occurs when element content # is specified to be ANY) if (child == None) : # the return list is set to all of the elements declared in the # DTD templist = dtd.get_elements() else : # if the penultimate element of the complex tuple is a list, # then we have to recursively process each element of the list. if type(child[-2])==type([]): for c in child[-2]: templist = getElementsName(c,dtd,templist) # if the penultimate element of the complex tuple is a tuple, # then we have to recursively process this last tuple. elif type(child[-2])==type(()): templist = getElementsName(child[-2],dtd,templist) # else the penultimate element of the complex tuple is a string # containing an allowed element name. We just have to append it # the return list. else: templist.append(child[-2]) return templist def element_to_string(element,pretty=1,encoding='ISO-8859-1') : return elements_to_string([element],pretty,encoding) def elements_to_string(elements,pretty=1,encoding='ISO-8859-1') : s = StringIO() s.write('\n'%encoding) if pretty: for element in elements: PrettyPrint(element, s, encoding=encoding) else: for element in elements : Print(element, s, encoding=encoding) str = s.getvalue() s.close() return str class XmlEditor(VBox) : """XmlEditor""" def __init__(self,dtd,node,idref_attributes_readonly=1): VBox.__init__(self) self.idref_attributes_readonly = idref_attributes_readonly self.node_unmodified=node self.node=node.cloneNode(1) self.dtd=dtd self.tree = XmlTree() scroll = ScrolledWindow() scroll.set_policy(POLICY_AUTOMATIC,POLICY_AUTOMATIC) scroll.add(self.tree) self.tree.set_expand_mode(1) self.tree.set_document(self.node) self.pack_start(scroll) self.editor = None self.set_editor('single line') self.tree.connect('button-press-event', self.tree_clicked) self.tree.connect('tree-select-row', self.item_selected) self.tooltips = Tooltips() def apply_changes(self): for (uri,qname) in self.node_unmodified.attributes.keys(): self.node_unmodified.removeAttributeNS(uri,qname) for attr in self.node.attributes.values(): self.node_unmodified.setAttributeNodeNS(attr.cloneNode(1)) for child in self.node_unmodified.childNodes[:]: self.node_unmodified.removeChild(child) ReleaseNode(child) for child in self.node.childNodes: self.node_unmodified.appendChild(child.cloneNode(1)) def set_editor(self, style, readonly=0, arg=None): if not self.editor: self.editor_style = '' if self.editor_style != style: expand = 0 self.editor_style = style assert style in ('single line', 'multi line', 'combo','node editor') if self.editor: self.remove(self.editor) self.editor.destroy() if style == 'single line': self.editor = Entry() self.editfield = self.editor self.editor_clear = self.entry_clear self.editor_get_text = self.entry_get_text self.editor_set_text = self.entry_set_text elif style == 'combo': self.editor = Combo() self.editor.set_value_in_list(1,0) self.editor.set_popdown_strings(arg) self.editfield = self.editor.entry self.editor_clear = self.entry_clear self.editor_get_text = self.entry_get_text self.editor_set_text = self.entry_set_text else: self.editfield = TextView() self.editor = ScrolledWindow() self.editor.set_policy(POLICY_AUTOMATIC, POLICY_AUTOMATIC) self.editor.add(self.editfield) self.editor_clear = self.textview_clear self.editor_get_text = self.textview_get_text self.editor_set_text = self.textview_set_text if style == 'node editor': expand = 1 self.tooltips.set_tip(self.editfield,'Edit the node. Finish with Ctrl-Return. Be careful, you are working on bare metal. Changing the ID of an element for example can have unpredictable results. You have been warned...''' ) self.editor.show_all() self.pack_start(self.editor, expand) if style != 'node editor': self.connect_id = self.editfield.connect('changed', self.item_edited) else: self.editfield.disconnect(self.connect_id) self.editor_clear() self.connect_id = self.editfield.connect('changed', self.item_edited) self.editfield.set_editable(not readonly) def entry_get_text(self): return self.editfield.get_text() def entry_set_text(self, value): self.editfield.set_text(value) def entry_clear(self): self.editfield.set_text('') def textview_get_text(self): return self.editfield.get_chars(0, -1) def textview_set_text(self, value): self.editfield.delete_text(0, -1) self.editfield.insert_text(value) def textview_clear(self): buf = self.editfield.get_buffer() buf.delete(*buf.get_bounds()) def item_edited(self, editor): treenode = editor.get_data('treenode') text = self.editor_get_text() node = self.tree.node_get_row_data(treenode) if node.nodeType == node.TEXT_NODE: node.data = text elif node.nodeType == node.ATTRIBUTE_NODE: node.value = text else: print 'Unknown node type', node.nodeType def element_hand_edited(self,editor): xml = self.editor_get_text() treenode = editor.get_data('treenode') xmlnode = editor.get_data('xmlnode') try: new_node = StripXml(Sax2.FromXml(xml, xmlnode.ownerDocument)) except Exception, ex: print ex else: xmlparent = xmlnode.parentNode xmlparent.replaceChild(new_node,xmlnode) def item_selected(self,tree,treenode,col): xmlnode = tree.node_get_row_data(treenode) if xmlnode.nodeType == xmlnode.ELEMENT_NODE: try: elt = self.dtd.get_elem(xmlnode.nodeName) except Exception: elt = None if elt and elt.get_content_model() == None and \ xmlnode not in map(lambda node,t=self.tree: t.node_get_row_data(node), self.tree.base_nodes()) : self.set_editor('node editor', 0) self.editfield.set_data('treenode', treenode) self.editfield.set_data('xmlnode', xmlnode) self.editor_set_text(element_to_string(xmlnode)) self.editfield.connect('activate', self.element_hand_edited) return else: self.set_editor('single line',1) elif xmlnode.nodeType == xmlnode.TEXT_NODE: self.set_editor('multi line',) elif xmlnode.nodeType == xmlnode.ATTRIBUTE_NODE: try: attr = self.dtd.get_elem(xmlnode.ownerElement.nodeName).get_attr(xmlnode.nodeName) except Exception: self.set_editor('single line',0) else: if attr and type(attr.get_type()) == type([]): self.set_editor('combo',0,map(utf8_to_gtk,attr.get_type())) # 0 is read-write else: self.set_editor('single line', attr.get_decl() == '#FIXED' or \ attr.get_type() == 'ID' or \ (attr.get_type() == 'IDREF' and self.idref_attributes_readonly)) # false is read-write self.editfield.set_data('treenode',treenode) self.editfield.set_data('xmlnode',xmlnode) self.editor_set_text(tree.node_get_text(treenode,2)) def tree_clicked(self,tree,event): if event.button == 3 and tree.get_selection_info(event.x,event.y): row,col = tree.get_selection_info(event.x,event.y) xmlnode = tree.get_row_data(row) treenode = tree.node_nth(row) popup = None if xmlnode.nodeType == xmlnode.ELEMENT_NODE: popup = self.buildElementPopup(xmlnode,treenode) elif xmlnode.nodeType == xmlnode.ATTRIBUTE_NODE: popup = self.buildAttributePopup(xmlnode,treenode) if popup: #tree.select_row(row,col) popup.show_all() popup.popup(None,None,None,3,event.time) def add_attribute_activated(self,item,xmlnode,treenode,attribute): uri = get_uri(xmlnode,get_prefix(attribute.get_name())) if not xmlnode.hasAttributeNS(uri,attribute.get_name()): value = attribute.get_default() or '' xmlnode.setAttributeNS(uri,attribute.get_name(),value) def delete_child_activated(self,item,xmlnode,treenode): xmlnode.parentNode.removeChild(xmlnode) ReleaseNode(xmlnode) def delete_attribute_activated(self,item,xmlnode,treenode): xmlnode.ownerElement.removeAttributeNode(xmlnode) ReleaseNode(xmlnode) def add_child_activated(self,item,xmlnode,treenode,tag,position): if tag == '#text': new_element = xmlnode.ownerDocument.createTextNode('') isleaf=1 else: uri = get_uri(xmlnode,get_prefix(tag)) new_element = xmlnode.ownerDocument.createElementNS(uri,tag) isleaf=0 if position < 0 or position >= len(xmlnode.childNodes): xmlnode.appendChild(new_element) else: xmlnode.insertBefore(new_element,xmlnode.childNodes[position]) def buildAttributePopup(self,xmlnode,treenode): try: attr = self.dtd.get_elem(xmlnode.ownerElement.tagName).get_attr(xmlnode.nodeName) except Exception: attr = None if attr == None or attr.get_decl() != '#REQUIRED': popup = Menu() item = MenuItem('delete') item.connect('activate',self.delete_attribute_activated, xmlnode,treenode) popup.append(item) return popup def buildElementPopup(self,xmlnode,treenode): popup = Menu() try: elt = self.dtd.get_elem(xmlnode.nodeName) except Exception: # the element is not known by the DTD popup.append(MenuItem()) item = MenuItem('Delete') item.connect('activate',self.delete_child_activated, xmlnode,treenode) popup.append(item) return item = MenuItem('Add attribute') popup.append(item) submenu = Menu() item.set_submenu(submenu) for attr in elt.get_attr_list(): subitem = MenuItem(attr) submenu.append(subitem) subitem.connect('activate',self.add_attribute_activated, xmlnode,treenode,elt.get_attr(attr)) item = MenuItem('Append Child') popup.append(item) submenu = self.__make_add_child_submenu(xmlnode,treenode,-1) item.set_submenu(submenu) item = MenuItem('Prepend Child') popup.append(item) submenu = self.__make_add_child_submenu(xmlnode,treenode,0) item.set_submenu(submenu) if xmlnode not in map(lambda node,t=self.tree: t.node_get_row_data(node), self.tree.base_nodes()): parentnode = xmlnode.parentNode index = parentnode.childNodes.index(xmlnode) item = MenuItem('Insert sibling before') popup.append(item) submenu = self.__make_add_child_submenu(parentnode,treenode.parent,index) item.set_submenu(submenu) item = MenuItem('Insert sibling after') popup.append(item) submenu = self.__make_add_child_submenu(parentnode,treenode.parent,index+1) item.set_submenu(submenu) popup.append(MenuItem()) item = MenuItem('Delete') item.connect('activate',self.delete_child_activated, xmlnode,treenode) popup.append(item) return popup def __make_add_child_submenu(self,xmlnode,treenode,position): submenu = Menu() elt = self.dtd.get_elem(xmlnode.nodeName) if elt.get_content_model(): # Compute current state state = elt.get_start_state() for child in xmlnode.childNodes[:position]: state = elt.next_state(state,child.nodeName) authorized = elt.get_valid_elements(state) else: # ANY content model authorized = self.dtd.get_elements() possible = getElementsName(elt.get_content_model(),self.dtd) for c in authorized: if c == '#PCDATA': c = '#text' subitem = MenuItem(c) subitem.connect('activate',self.add_child_activated, xmlnode,treenode,c,position) submenu.append(subitem) if len(possible)>len(authorized): submenu.append(MenuItem()) for c in possible: if c not in authorized: if c == '#PCDATA': c = '#text' subitem = MenuItem(c) subitem.connect('activate',self.add_child_activated, xmlnode,treenode,c,position) submenu.append(subitem) return submenu class XmlEditorDialog(Dialog): """XmlEditorDialog""" def __init__(self,title,dtd,node): Dialog.__init__(self) self.set_title(title) self.editor = XmlEditor(dtd,node) self.vbox.pack_start(self.editor) self.ok_button=Button('OK') self.ok_button.set_flags(CAN_DEFAULT|HAS_DEFAULT) self.apply_button=Button('Apply') self.apply_button.set_flags(CAN_DEFAULT) self.cancel_button=Button('Cancel') self.cancel_button.set_flags(CAN_DEFAULT) self.action_area.pack_start(self.ok_button) self.action_area.pack_start(self.apply_button) self.action_area.pack_start(self.cancel_button) self.ok_button.connect('clicked', self.ok_button_clicked) self.apply_button.connect('clicked', self.apply_button_clicked) self.cancel_button.connect('clicked', self.cancel_button_clicked) self.set_size_request(350, 300) self.change_listeners = [] def add_change_listener(self,listener,*args): self.change_listeners.append((listener,args)) def ok_button_clicked(self,button): self.apply_button_clicked(button) self.cancel_button_clicked(button) def apply_button_clicked(self,button): self.editor.apply_changes() for (l,args) in self.change_listeners: apply(l,args) def cancel_button_clicked(self,button): self.destroy() class XmlEditorWindow(Window): """XmlEditorWindow (inherits from Window and pass)""" pass # test ######################################################################### if __name__=='__main__': import sys if len(sys.argv)!=3 : print "XmlEditor.py file dtd" sys.exit(1) dtd = parse_dtd_file(sys.argv[2]) domtree=Sax2.FromXmlFile(sys.argv[1]).documentElement window=XmlEditorDialog('Edition of '+sys.argv[1], dtd, domtree) window.connect('delete_event',lambda x, e:mainquit()) window.show_all() mainloop() stream = open(sys.argv[1],'w') PrettyPrint(domtree.ownerDocument, stream=stream) stream.close()