# 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()