Removed all the MD5 suck checking stuff.
[dom-editor.git] / Dome / Model.py
blob34ac63a44d2c4459b29062355f1401ee419754e6
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 def load_ns(self, node):
104 assert node.localName == 'namespaces'
105 assert node.namespaceURI == constants.DOME_NS
106 for x in node.childNodes:
107 if x.nodeType != Node.ELEMENT_NODE: continue
108 if x.localName != 'ns': continue
109 if x.namespaceURI != constants.DOME_NS: continue
111 self.namespaces.ensure_ns(x.getAttributeNS(None, 'prefix'),
112 x.getAttributeNS(None, 'uri'))
114 def import_with_ns(self, node):
115 """Return a copy of node for this model. All namespaces used in the subtree
116 will have been added to the global namespaces list. Prefixes will have been changed
117 as required to avoid conflicts."""
118 doc = self.doc
119 def ns_clone(node, clone):
120 if node.nodeType != Node.ELEMENT_NODE:
121 return doc.importNode(node, 1)
122 if node.namespaceURI:
123 prefix = self.namespaces.ensure_ns(node.prefix, node.namespaceURI)
124 new = doc.createElementNS(node.namespaceURI,
125 prefix + ':' + node.localName)
126 else:
127 new = doc.createElementNS(None, node.localName)
128 for a in node.attributes.values():
129 if a.namespaceURI == XMLNS_NAMESPACE: continue
130 new.setAttributeNS(a.namespaceURI, a.name, a.value)
131 for k in node.childNodes:
132 new.appendChild(clone(k, clone))
133 return new
134 new = ns_clone(node, ns_clone)
135 return new
137 def clear_undo(self):
138 # Pop an (op_number, function) off one of these and call the function to
139 # move forwards or backwards in the undo history.
140 self.undo_stack = []
141 self.redo_stack = []
142 self.doing_undo = 0
144 # Each series of suboperations in an undo stack which are part of a single
145 # user op will have the same number...
146 self.user_op = 1
148 #import gc
149 #print "GC freed:", gc.collect()
150 #print "Garbage", gc.garbage
152 def lock(self, node):
153 """Prevent removal of this node (or any ancestor)."""
154 #print "Locking", node.nodeName
155 assert node.nodeType
156 self.locks[node] = self.get_locks(node) + 1
157 if node.parentNode:
158 self.lock(node.parentNode)
160 def unlock(self, node):
161 """Reverse the effect of lock(). Must call unlock the same number
162 of times as lock to fully unlock the node."""
163 l = self.get_locks(node)
164 if l > 1:
165 self.locks[node] = l - 1
166 elif l == 1:
167 del self.locks[node] # Or get a memory leak...
168 if node == self.doc.documentElement:
169 if node.parentNode:
170 self.unlock(node.parentNode)
171 return
172 else:
173 raise Exception('unlock(%s): Node not locked!' % node)
174 if node.parentNode:
175 self.unlock(node.parentNode)
177 def get_locks(self, node):
178 try:
179 return self.locks[node]
180 except KeyError:
181 return 0
183 def lock_and_copy(self, node):
184 """Locks 'node' in the current model and returns a new model
185 with a copy of the subtree."""
186 if self.get_locks(node):
187 raise Exception("Can't enter locked node!")
188 m = Model(self.get_base_uri(node), root_program = self.root_program, do_load = 0)
189 copy = m.import_with_ns(node)
190 root = m.get_root()
191 m.replace_node(root, copy)
192 self.lock(node)
193 return m
195 def mark(self):
196 "Increment the user_op counter. Undo will undo every operation between"
197 "two marks."
198 self.user_op += 1
200 def get_root(self):
201 "Return the true root node (not a view root)"
202 return self.doc.documentElement
204 def add_view(self, view):
205 "'view' provides:"
206 "'update_all(subtree) - called when a major change occurs."
207 #print "New view:", view
208 self.views.append(view)
210 def remove_view(self, view):
211 #print "Removing view", view
212 self.views.remove(view)
213 #print "Now:", self.views
214 if not self.views:
215 self.clear_undo()
217 def update_all(self, node):
218 "Called when 'node' has been updated."
219 "'node' is still in the document, so deleting or replacing"
220 "a node calls this on the parent."
221 for v in self.views:
222 v.update_all(node)
224 def update_replace(self, old, new):
225 "Called when 'old' is replaced by 'new'."
226 for v in self.views:
227 v.update_replace(old, new)
229 def strip_space(self, node = None):
230 if not node:
231 node = self.doc.documentElement
232 def ss(node):
233 if node.nodeType == Node.TEXT_NODE:
234 #node.data = node.data.strip()
235 #if node.data == '':
236 # node.parentNode.removeChild(node)
237 if not node.data.strip():
238 node.parentNode.removeChild(node)
239 else:
240 for k in node.childNodes[:]:
241 ss(k)
242 ss(node)
244 # Changes
246 def normalise(self, node):
247 old = node.cloneNode(1)
248 node.normalize()
249 self.add_undo(lambda: self.replace_node(node, old))
250 self.update_all(node)
252 def convert_to_element(self, node):
253 assert node.nodeType in (Node.COMMENT_NODE, Node.PROCESSING_INSTRUCTION_NODE,
254 Node.TEXT_NODE)
255 new = self.doc.createElementNS(None, node.data.strip())
256 self.replace_node(node, new)
257 return new
259 def convert_to_text(self, node):
260 assert node.nodeType in (Node.COMMENT_NODE, Node.PROCESSING_INSTRUCTION_NODE,
261 Node.TEXT_NODE, Node.ELEMENT_NODE)
262 if node.nodeType == Node.ELEMENT_NODE:
263 new = self.doc.createTextNode(node.localName)
264 else:
265 new = self.doc.createTextNode(node.data)
266 self.replace_node(node, new)
267 return new
269 def convert_to_comment(self, node):
270 assert node.nodeType in (Node.COMMENT_NODE, Node.PROCESSING_INSTRUCTION_NODE,
271 Node.TEXT_NODE)
272 new = self.doc.createComment(node.data)
273 self.replace_node(node, new)
274 return new
276 def remove_ns(self, node):
277 print "remove_ns: Shouldn't need this now!"
278 return
280 nss = GetAllNs(node.parentNode)
281 dns = nss.get(None, None)
282 create = node.ownerDocument.createElementNS
283 # clone is an argument to fix a wierd gc bug in python2.2
284 def ns_clone(node, clone):
285 if node.nodeType != Node.ELEMENT_NODE:
286 return node.cloneNode(1)
287 new = create(dns, node.nodeName)
288 for a in node.attributes.values():
289 if a.localName == 'xmlns' and a.prefix is None:
290 print "Removing xmlns attrib on", node
291 continue
292 new.setAttributeNS(a.namespaceURI, a.name, a.value)
293 for k in node.childNodes:
294 new.appendChild(clone(k, clone))
295 return new
296 new = ns_clone(node, ns_clone)
297 self.replace_node(node, new)
298 return new
300 def set_name(self, node, namespace, name):
301 if self.get_locks(node):
302 raise Exception('Attempt to set name on locked node %s' % node)
304 new = node.ownerDocument.createElementNS(namespace, name)
305 self.replace_shallow(node, new)
306 return new
308 def replace_shallow(self, old, new):
309 """Replace old with new, keeping the old children."""
310 assert not new.childNodes
311 assert not new.parentNode
313 old_name = old.nodeName
314 old_ns = old.namespaceURI
316 kids = old.childNodes[:]
317 attrs = old.attributes.values()
318 parent = old.parentNode
319 [ old.removeChild(k) for k in kids ]
320 parent.replaceChild(new, old)
321 [ new.appendChild(k) for k in kids ]
322 [ new.setAttributeNS(a.namespaceURI, a.name, a.value) for a in attrs ]
324 self.add_undo(lambda: self.replace_shallow(new, old))
326 self.update_replace(old, new)
328 import __main__
329 if __main__.no_gui_mode:
330 def add_undo(self, fn):
331 pass
332 else:
333 def add_undo(self, fn):
334 self.undo_stack.append((self.user_op, fn))
335 if not self.doing_undo:
336 self.redo_stack = []
338 def set_data(self, node, data):
339 old_data = node.data
340 node.data = data
341 self.add_undo(lambda: self.set_data(node, old_data))
342 self.update_all(node)
344 def replace_node(self, old, new):
345 if self.get_locks(old):
346 raise Exception('Attempt to replace locked node %s' % old)
347 old.parentNode.replaceChild(new, old)
348 self.add_undo(lambda: self.replace_node(new, old))
350 self.update_replace(old, new)
352 def delete_shallow(self, node):
353 """Replace node with its contents"""
354 kids = node.childNodes[:]
355 next = node.nextSibling
356 parent = node.parentNode
357 for n in kids + [node]:
358 if self.get_locks(n):
359 raise Exception('Attempt to move/delete locked node %s' % n)
360 for k in kids:
361 self.delete_internal(k)
362 self.delete_internal(node)
363 for k in kids:
364 self.insert_before_interal(next, k, parent)
365 self.update_all(parent)
367 def delete_nodes(self, nodes):
368 #print "Deleting", nodes
369 for n in nodes:
370 if self.get_locks(n):
371 raise Exception('Attempt to delete locked node %s' % n)
372 for n in nodes:
373 p = n.parentNode
374 self.delete_internal(n)
375 self.update_all(p)
377 def delete_internal(self, node):
378 "Doesn't update display."
379 next = node.nextSibling
380 parent = node.parentNode
381 parent.removeChild(node)
382 self.add_undo(lambda: self.insert_before(next, node, parent = parent))
384 def insert_before_interal(self, node, new, parent):
385 "Insert 'new' before 'node'. If 'node' is None then insert at the end"
386 "of parent's children."
387 assert new.nodeType != Node.DOCUMENT_FRAGMENT_NODE
388 assert parent.nodeType == Node.ELEMENT_NODE
389 parent.insertBefore(new, node)
390 self.add_undo(lambda: self.delete_nodes([new]))
392 def undo(self):
393 if not self.undo_stack:
394 raise Exception('Nothing to undo')
396 assert not self.doing_undo
398 uop = self.undo_stack[-1][0]
400 # Swap stacks so that the undo actions will populate the redo stack...
401 (self.undo_stack, self.redo_stack) = (self.redo_stack, self.undo_stack)
402 self.doing_undo = 1
403 try:
404 while self.redo_stack and self.redo_stack[-1][0] == uop:
405 self.redo_stack[-1][1]()
406 self.redo_stack.pop()
407 finally:
408 (self.undo_stack, self.redo_stack) = (self.redo_stack, self.undo_stack)
409 self.doing_undo = 0
411 def redo(self):
412 if not self.redo_stack:
413 raise Exception('Nothing to redo')
415 uop = self.redo_stack[-1][0]
416 self.doing_undo = 1
417 try:
418 while self.redo_stack and self.redo_stack[-1][0] == uop:
419 self.redo_stack[-1][1]()
420 self.redo_stack.pop()
421 finally:
422 self.doing_undo = 0
424 def insert(self, node, new, index = 0):
425 if len(node.childNodes) > index:
426 self.insert_before(node.childNodes[index], new)
427 else:
428 self.insert_before(None, new, parent = node)
430 def insert_after(self, node, new):
431 self.insert_before(node.nextSibling, new, parent = node.parentNode)
433 def insert_before(self, node, new, parent = None):
434 "Insert 'new' before 'node'. If 'node' is None then insert at the end"
435 "of parent's children."
436 if not parent:
437 parent = node.parentNode
438 if new.nodeType == Node.DOCUMENT_FRAGMENT_NODE:
439 for n in new.childNodes[:]:
440 self.insert_before_interal(node, n, parent)
441 else:
442 self.insert_before_interal(node, new, parent)
443 self.update_all(parent)
445 def split_qname(self, node, name):
446 if name == 'xmlns':
447 namespaceURI = XMLNS_NAMESPACE
448 localName = name
449 elif ':' in name:
450 prefix, localName = name.split(':')
451 namespaceURI = self.prefix_to_namespace(node, prefix)
452 else:
453 namespaceURI = None
454 localName = name
455 return namespaceURI, localName
457 def set_attrib(self, node, name, value, with_update = 1):
458 """Set an attribute's value. If value is None, remove the attribute.
459 Returns the new attribute node, or None if removing."""
460 namespaceURI, localName = self.split_qname(node, name)
462 if node.hasAttributeNS(namespaceURI, localName):
463 old = node.getAttributeNS(namespaceURI, localName)
464 else:
465 old = None
466 #print "Set (%s,%s) = %s" % (namespaceURI, name, value)
467 if value != None:
468 node.setAttributeNS(namespaceURI, name, value)
469 else:
470 node.removeAttributeNS(namespaceURI, localName)
472 self.add_undo(lambda: self.set_attrib(node, name, old))
474 if with_update:
475 self.update_all(node)
476 if value != None:
477 if localName == 'xmlns':
478 localName = None
479 return node.attributes[(namespaceURI, localName)]
481 def prefix_to_namespace(self, node, prefix):
482 "Using attributes for namespaces was too confusing. Keep a global list instead."
483 if prefix is None: return None
484 try:
485 return self.namespaces.uri[prefix]
486 except KeyError:
487 raise Exception("Namespace '%s' is not defined. Choose "
488 "View->Show namespaces from the popup menu to set it." % prefix)
490 "Use the xmlns attributes to workout the namespace."
491 nss = GetAllNs(node)
492 if nss.has_key(prefix):
493 return nss[prefix] or None
494 if prefix:
495 if prefix == 'xmlns':
496 return XMLNS_NAMESPACE
497 raise Exception("No such namespace prefix '%s'" % prefix)
498 else:
499 return None
501 def get_base_uri(self, node):
502 """Go up through the parents looking for a uri attribute.
503 If there isn't one, use the document's URI."""
504 while node:
505 if node.nodeType == Node.DOCUMENT_NODE:
506 return self.uri
507 if node.hasAttributeNS(None, 'uri'):
508 return node.getAttributeNS(None, 'uri')
509 node = node.parentNode
510 return None
512 def load_html(self, path):
513 """Load this HTML file and return the new document."""
514 data = file(path).read()
515 data = support.to_html_doc(data)
516 doc = support.parse_data(data, path)
517 self.strip_space(doc)
518 return doc