Improved UI for 'Select by XPath'.
[dom-editor.git] / Dome / Model.py
blob3f667dae091e554cae8460ba8a16408f61d18fd1
1 from __future__ import nested_scopes
3 # An model contains:
4 # - A DOM document
5 # - The undo history
6 # - The root program
7 # All changes to the DOM must go through here.
8 # Notification to views of changes is done.
10 from Ft.Xml.cDomlette import implementation, nonvalParse
11 from Ft.Xml.Domlette import GetAllNs
12 from Ft.Xml import XMLNS_NAMESPACE
14 from xml.dom import Node
15 import support
16 from Beep import Beep
17 import constants
19 class Model:
20 def __init__(self, path, root_program = None, dome_data = None, do_load = 1):
21 "If root_program is given, then no data is loaded (used for lock_and_copy)."
22 self.uri = 'Prog.dome'
23 import Namespaces
24 self.namespaces = Namespaces.Namespaces()
26 if dome_data:
27 from Ft.Xml.InputSource import InputSourceFactory
28 isrc = InputSourceFactory()
29 dome_data = nonvalParse(isrc.fromUri(dome_data))
31 self.clear_undo()
33 doc = None
34 if path:
35 if path != '-':
36 self.uri = path
37 if do_load and (path.endswith('.html') or path.endswith('.htm')):
38 doc = self.load_html(path)
39 if not doc and not root_program:
40 from Ft.Xml.InputSource import InputSourceFactory
41 isrc = InputSourceFactory()
42 try:
43 doc = nonvalParse(isrc.fromUri(path))
44 except:
45 import rox
46 rox.report_exception()
47 if not doc:
48 doc = implementation.createDocument(None, 'root', None)
49 root = doc.documentElement
51 self.root_program = None
52 data_to_load = None
54 from Program import Program, load_dome_program
55 if root.namespaceURI == constants.DOME_NS and root.localName == 'dome':
56 for x in root.childNodes:
57 if x.namespaceURI == constants.DOME_NS:
58 if x.localName == 'namespaces':
59 self.load_ns(x)
60 elif x.localName == 'dome-program':
61 self.root_program = load_dome_program(x,
62 self.namespaces)
63 elif x.localName == 'dome-data':
64 for y in x.childNodes:
65 if y.nodeType == Node.ELEMENT_NODE:
66 data_to_load = y
67 if dome_data:
68 data_to_load = dome_data.documentElement
69 elif (root.namespaceURI == constants.XSLT_NS and
70 root.localName in ['stylesheet', 'transform']) or \
71 root.hasAttributeNS(constants.XSLT_NS, 'version'):
72 import xslt
73 self.root_program = xslt.import_sheet(doc)
74 x = implementation.createDocument(None, 'xslt', None)
75 data_to_load = x.documentElement
76 src = doc.createElementNS(None, 'Source')
77 if file:
78 # TODO: import_with_ns?
79 src.appendChild(self.import_with_ns(doc,
80 dome_data.documentElement))
81 data_to_load.appendChild(x.createElementNS(None, 'Result'))
82 data_to_load.appendChild(src)
83 dome_data = None
84 else:
85 data_to_load = root
87 if root_program:
88 self.root_program = root_program
89 else:
90 if not self.root_program:
91 self.root_program = Program('Root')
93 if data_to_load:
94 self.doc = implementation.createDocument(None, 'root', None)
95 if not root_program:
96 node = self.import_with_ns(data_to_load)
97 self.doc.replaceChild(node, self.doc.documentElement)
98 self.strip_space()
100 self.views = [] # Notified when something changes
101 self.locks = {} # Node -> number of locks
103 self.hidden = {} # Node -> Message
104 self.hidden_code = {} # XPath to generate Message, if any
106 def load_ns(self, node):
107 assert node.localName == 'namespaces'
108 assert node.namespaceURI == constants.DOME_NS
109 for x in node.childNodes:
110 if x.nodeType != Node.ELEMENT_NODE: continue
111 if x.localName != 'ns': continue
112 if x.namespaceURI != constants.DOME_NS: continue
114 self.namespaces.ensure_ns(x.getAttributeNS(None, 'prefix'),
115 x.getAttributeNS(None, 'uri'))
117 def import_with_ns(self, node):
118 """Return a copy of node for this model. All namespaces used in the subtree
119 will have been added to the global namespaces list. Prefixes will have been changed
120 as required to avoid conflicts."""
121 doc = self.doc
122 def ns_clone(node, clone):
123 if node.nodeType != Node.ELEMENT_NODE:
124 return doc.importNode(node, 1)
125 if node.namespaceURI:
126 prefix = self.namespaces.ensure_ns(node.prefix, node.namespaceURI)
127 new = doc.createElementNS(node.namespaceURI,
128 prefix + ':' + node.localName)
129 else:
130 new = doc.createElementNS(None, node.localName)
131 for a in node.attributes.values():
132 if a.namespaceURI == XMLNS_NAMESPACE: continue
133 new.setAttributeNS(a.namespaceURI, a.name, a.value)
134 for k in node.childNodes:
135 new.appendChild(clone(k, clone))
136 return new
137 new = ns_clone(node, ns_clone)
138 return new
140 def clear_undo(self):
141 # Pop an (op_number, function) off one of these and call the function to
142 # move forwards or backwards in the undo history.
143 self.undo_stack = []
144 self.redo_stack = []
145 self.doing_undo = 0
147 # Each series of suboperations in an undo stack which are part of a single
148 # user op will have the same number...
149 self.user_op = 1
151 #import gc
152 #print "GC freed:", gc.collect()
153 #print "Garbage", gc.garbage
155 def lock(self, node):
156 """Prevent removal of this node (or any ancestor)."""
157 #print "Locking", node.nodeName
158 assert node.nodeType
159 self.locks[node] = self.get_locks(node) + 1
160 if node.parentNode:
161 self.lock(node.parentNode)
163 def unlock(self, node):
164 """Reverse the effect of lock(). Must call unlock the same number
165 of times as lock to fully unlock the node."""
166 l = self.get_locks(node)
167 if l > 1:
168 self.locks[node] = l - 1
169 elif l == 1:
170 del self.locks[node] # Or get a memory leak...
171 if node == self.doc.documentElement:
172 if node.parentNode:
173 self.unlock(node.parentNode)
174 return
175 else:
176 raise Exception('unlock(%s): Node not locked!' % node)
177 if node.parentNode:
178 self.unlock(node.parentNode)
180 def get_locks(self, node):
181 try:
182 return self.locks[node]
183 except KeyError:
184 return 0
186 def lock_and_copy(self, node):
187 """Locks 'node' in the current model and returns a new model
188 with a copy of the subtree."""
189 if self.get_locks(node):
190 raise Exception("Can't enter locked node!")
191 m = Model(self.get_base_uri(node), root_program = self.root_program, do_load = 0)
192 copy = m.import_with_ns(node)
193 root = m.get_root()
194 m.replace_node(root, copy)
195 self.lock(node)
196 return m
198 def mark(self):
199 "Increment the user_op counter. Undo will undo every operation between"
200 "two marks."
201 self.user_op += 1
203 def get_root(self):
204 "Return the true root node (not a view root)"
205 return self.doc.documentElement
207 def add_view(self, view):
208 "'view' provides:"
209 "'update_all(subtree) - called when a major change occurs."
210 #print "New view:", view
211 self.views.append(view)
213 def remove_view(self, view):
214 #print "Removing view", view
215 self.views.remove(view)
216 #print "Now:", self.views
217 if not self.views:
218 self.clear_undo()
220 def update_all(self, node):
221 "Called when 'node' has been updated."
222 "'node' is still in the document, so deleting or replacing"
223 "a node calls this on the parent."
224 for v in self.views:
225 v.update_all(node)
227 def update_replace(self, old, new):
228 "Called when 'old' is replaced by 'new'."
229 for v in self.views:
230 v.update_replace(old, new)
232 def strip_space(self, node = None):
233 if not node:
234 node = self.doc.documentElement
235 def ss(node):
236 if node.nodeType == Node.TEXT_NODE:
237 #node.data = node.data.strip()
238 #if node.data == '':
239 # node.parentNode.removeChild(node)
240 if not node.data.strip():
241 node.parentNode.removeChild(node)
242 else:
243 for k in node.childNodes[:]:
244 ss(k)
245 ss(node)
247 # Changes
249 def normalise(self, node):
250 old = node.cloneNode(1)
251 node.normalize()
252 self.add_undo(lambda: self.replace_node(node, old))
253 self.update_all(node)
255 def convert_to_element(self, node):
256 assert node.nodeType in (Node.COMMENT_NODE, Node.PROCESSING_INSTRUCTION_NODE,
257 Node.TEXT_NODE)
258 new = self.doc.createElementNS(None, node.data.strip())
259 self.replace_node(node, new)
260 return new
262 def convert_to_text(self, node):
263 assert node.nodeType in (Node.COMMENT_NODE, Node.PROCESSING_INSTRUCTION_NODE,
264 Node.TEXT_NODE, Node.ELEMENT_NODE)
265 if node.nodeType == Node.ELEMENT_NODE:
266 new = self.doc.createTextNode(node.localName)
267 else:
268 new = self.doc.createTextNode(node.data)
269 self.replace_node(node, new)
270 return new
272 def convert_to_comment(self, node):
273 assert node.nodeType in (Node.COMMENT_NODE, Node.PROCESSING_INSTRUCTION_NODE,
274 Node.TEXT_NODE)
275 new = self.doc.createComment(node.data)
276 self.replace_node(node, new)
277 return new
279 def remove_ns(self, node):
280 print "remove_ns: Shouldn't need this now!"
281 return
283 nss = GetAllNs(node.parentNode)
284 dns = nss.get(None, None)
285 create = node.ownerDocument.createElementNS
286 # clone is an argument to fix a wierd gc bug in python2.2
287 def ns_clone(node, clone):
288 if node.nodeType != Node.ELEMENT_NODE:
289 return node.cloneNode(1)
290 new = create(dns, node.nodeName)
291 for a in node.attributes.values():
292 if a.localName == 'xmlns' and a.prefix is None:
293 print "Removing xmlns attrib on", node
294 continue
295 new.setAttributeNS(a.namespaceURI, a.name, a.value)
296 for k in node.childNodes:
297 new.appendChild(clone(k, clone))
298 return new
299 new = ns_clone(node, ns_clone)
300 self.replace_node(node, new)
301 return new
303 def set_name(self, node, namespace, name):
304 if self.get_locks(node):
305 raise Exception('Attempt to set name on locked node %s' % node)
307 new = node.ownerDocument.createElementNS(namespace, name)
308 self.replace_shallow(node, new)
309 return new
311 def replace_shallow(self, old, new):
312 """Replace old with new, keeping the old children."""
313 assert not new.childNodes
314 assert not new.parentNode
316 old_name = old.nodeName
317 old_ns = old.namespaceURI
319 kids = old.childNodes[:]
320 attrs = old.attributes.values()
321 parent = old.parentNode
322 [ old.removeChild(k) for k in kids ]
323 parent.replaceChild(new, old)
324 [ new.appendChild(k) for k in kids ]
325 [ new.setAttributeNS(a.namespaceURI, a.name, a.value) for a in attrs ]
327 self.add_undo(lambda: self.replace_shallow(new, old))
329 self.update_replace(old, new)
331 import __main__
332 if __main__.no_gui_mode:
333 def add_undo(self, fn):
334 pass
335 else:
336 def add_undo(self, fn):
337 self.undo_stack.append((self.user_op, fn))
338 if not self.doing_undo:
339 self.redo_stack = []
341 def set_data(self, node, data):
342 old_data = node.data
343 node.data = data
344 self.add_undo(lambda: self.set_data(node, old_data))
345 self.update_all(node)
347 def replace_node(self, old, new):
348 if self.get_locks(old):
349 raise Exception('Attempt to replace locked node %s' % old)
350 old.parentNode.replaceChild(new, old)
351 self.add_undo(lambda: self.replace_node(new, old))
353 self.update_replace(old, new)
355 def delete_shallow(self, node):
356 """Replace node with its contents"""
357 kids = node.childNodes[:]
358 next = node.nextSibling
359 parent = node.parentNode
360 for n in kids + [node]:
361 if self.get_locks(n):
362 raise Exception('Attempt to move/delete locked node %s' % n)
363 for k in kids:
364 self.delete_internal(k)
365 self.delete_internal(node)
366 for k in kids:
367 self.insert_before_interal(next, k, parent)
368 self.update_all(parent)
370 def delete_nodes(self, nodes):
371 #print "Deleting", nodes
372 for n in nodes:
373 if self.get_locks(n):
374 raise Exception('Attempt to delete locked node %s' % n)
375 for n in nodes:
376 p = n.parentNode
377 self.delete_internal(n)
378 self.update_all(p)
380 def delete_internal(self, node):
381 "Doesn't update display."
382 next = node.nextSibling
383 parent = node.parentNode
384 parent.removeChild(node)
385 self.add_undo(lambda: self.insert_before(next, node, parent = parent))
387 def insert_before_interal(self, node, new, parent):
388 "Insert 'new' before 'node'. If 'node' is None then insert at the end"
389 "of parent's children."
390 assert new.nodeType != Node.DOCUMENT_FRAGMENT_NODE
391 #assert parent.nodeType == Node.ELEMENT_NODE
392 parent.insertBefore(new, node)
393 self.add_undo(lambda: self.delete_nodes([new]))
395 def undo(self):
396 if not self.undo_stack:
397 raise Exception('Nothing to undo')
399 assert not self.doing_undo
401 uop = self.undo_stack[-1][0]
403 # Swap stacks so that the undo actions will populate the redo stack...
404 (self.undo_stack, self.redo_stack) = (self.redo_stack, self.undo_stack)
405 self.doing_undo = 1
406 try:
407 while self.redo_stack and self.redo_stack[-1][0] == uop:
408 self.redo_stack[-1][1]()
409 self.redo_stack.pop()
410 finally:
411 (self.undo_stack, self.redo_stack) = (self.redo_stack, self.undo_stack)
412 self.doing_undo = 0
414 def redo(self):
415 if not self.redo_stack:
416 raise Exception('Nothing to redo')
418 uop = self.redo_stack[-1][0]
419 self.doing_undo = 1
420 try:
421 while self.redo_stack and self.redo_stack[-1][0] == uop:
422 self.redo_stack[-1][1]()
423 self.redo_stack.pop()
424 finally:
425 self.doing_undo = 0
427 def insert(self, node, new, index = 0):
428 if len(node.childNodes) > index:
429 self.insert_before(node.childNodes[index], new)
430 else:
431 self.insert_before(None, new, parent = node)
433 def insert_after(self, node, new):
434 self.insert_before(node.nextSibling, new, parent = node.parentNode)
436 def insert_before(self, node, new, parent = None):
437 "Insert 'new' before 'node'. If 'node' is None then insert at the end"
438 "of parent's children."
439 if not parent:
440 parent = node.parentNode
441 if new.nodeType == Node.DOCUMENT_FRAGMENT_NODE:
442 for n in new.childNodes[:]:
443 self.insert_before_interal(node, n, parent)
444 else:
445 self.insert_before_interal(node, new, parent)
446 self.update_all(parent)
448 def split_qname(self, node, name):
449 if name == 'xmlns':
450 namespaceURI = XMLNS_NAMESPACE
451 localName = name
452 elif ':' in name:
453 prefix, localName = name.split(':')
454 namespaceURI = self.prefix_to_namespace(node, prefix)
455 else:
456 namespaceURI = None
457 localName = name
458 return namespaceURI, localName
460 def set_attrib(self, node, name, value, with_update = 1):
461 """Set an attribute's value. If value is None, remove the attribute.
462 Returns the new attribute node, or None if removing."""
463 namespaceURI, localName = self.split_qname(node, name)
465 if node.hasAttributeNS(namespaceURI, localName):
466 old = node.getAttributeNS(namespaceURI, localName)
467 else:
468 old = None
469 #print "Set (%s,%s) = %s" % (namespaceURI, name, value)
470 if value != None:
471 node.setAttributeNS(namespaceURI, name, value)
472 else:
473 node.removeAttributeNS(namespaceURI, localName)
475 self.add_undo(lambda: self.set_attrib(node, name, old))
477 if with_update:
478 self.update_all(node)
479 if value != None:
480 if localName == 'xmlns':
481 localName = None
482 return node.attributes[(namespaceURI, localName)]
484 def prefix_to_namespace(self, node, prefix):
485 "Using attributes for namespaces was too confusing. Keep a global list instead."
486 if prefix is None: return None
487 try:
488 return self.namespaces.uri[prefix]
489 except KeyError:
490 raise Exception("Namespace '%s' is not defined. Choose "
491 "View->Show namespaces from the popup menu to set it." % prefix)
493 "Use the xmlns attributes to workout the namespace."
494 nss = GetAllNs(node)
495 if nss.has_key(prefix):
496 return nss[prefix] or None
497 if prefix:
498 if prefix == 'xmlns':
499 return XMLNS_NAMESPACE
500 raise Exception("No such namespace prefix '%s'" % prefix)
501 else:
502 return None
504 def get_base_uri(self, node):
505 """Go up through the parents looking for a uri attribute.
506 If there isn't one, use the document's URI."""
507 while node:
508 if node.nodeType == Node.DOCUMENT_NODE:
509 return self.uri
510 if node.hasAttributeNS(None, 'uri'):
511 return node.getAttributeNS(None, 'uri')
512 node = node.parentNode
513 return None
515 def load_html(self, path):
516 """Load this HTML file and return the new document."""
517 data = file(path).read()
518 data = support.to_html_doc(data)
519 doc = support.parse_data(data, path)
520 self.strip_space(doc)
521 return doc