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