Fixed version number (for 0release)
[dom-editor.git] / Dome / Model.py
blobc7df17b3d73fd0c6267f81a41463bd709a41ac4d
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 print root.namespaceURI, root.localName
56 if root.namespaceURI == constants.DOME_NS and root.localName == 'dome':
57 for x in root.childNodes:
58 if x.namespaceURI == constants.DOME_NS:
59 if x.localName == 'namespaces':
60 self.load_ns(x)
61 elif x.localName == 'dome-program':
62 self.root_program = load_dome_program(x,
63 self.namespaces)
64 elif x.localName == 'dome-data':
65 for y in x.childNodes:
66 if y.nodeType == Node.ELEMENT_NODE:
67 data_to_load = y
68 if dome_data:
69 data_to_load = dome_data.documentElement
70 elif (root.namespaceURI == constants.XSLT_NS and
71 root.localName in ['stylesheet', 'transform']) or \
72 root.hasAttributeNS(constants.XSLT_NS, 'version'):
73 import xslt
74 self.root_program = xslt.import_sheet(doc)
75 self.doc = implementation.createDocument(None, 'xslt', None)
76 data_to_load = None
77 src = self.doc.createElementNS(None, 'Source')
78 if dome_data:
79 src.appendChild(self.import_with_ns(dome_data.documentElement))
80 self.doc.documentElement.appendChild(src)
81 self.doc.documentElement.appendChild(self.doc.createElementNS(None, 'Result'))
82 self.strip_space()
83 data_to_load = None
84 dome_data = None
85 else:
86 data_to_load = root
88 if root_program:
89 self.root_program = root_program
90 else:
91 if not self.root_program:
92 self.root_program = Program('Root')
94 if data_to_load:
95 self.doc = implementation.createDocument(None, 'root', None)
96 if not root_program:
97 node = self.import_with_ns(data_to_load)
98 self.doc.replaceChild(node, self.doc.documentElement)
99 self.strip_space()
101 self.views = [] # Notified when something changes
102 self.locks = {} # Node -> number of locks
104 self.hidden = {} # Node -> Message
105 self.hidden_code = {} # XPath to generate Message, if any
107 def load_ns(self, node):
108 assert node.localName == 'namespaces'
109 assert node.namespaceURI == constants.DOME_NS
110 for x in node.childNodes:
111 if x.nodeType != Node.ELEMENT_NODE: continue
112 if x.localName != 'ns': continue
113 if x.namespaceURI != constants.DOME_NS: continue
115 self.namespaces.ensure_ns(x.getAttributeNS(None, 'prefix'),
116 x.getAttributeNS(None, 'uri'))
118 def import_with_ns(self, node):
119 """Return a copy of node for this model. All namespaces used in the subtree
120 will have been added to the global namespaces list. Prefixes will have been changed
121 as required to avoid conflicts."""
122 doc = self.doc
123 def ns_clone(node, clone):
124 if node.nodeType != Node.ELEMENT_NODE:
125 return doc.importNode(node, 1)
126 if node.namespaceURI:
127 prefix = self.namespaces.ensure_ns(node.prefix, node.namespaceURI)
128 new = doc.createElementNS(node.namespaceURI,
129 prefix + ':' + node.localName)
130 else:
131 new = doc.createElementNS(None, node.localName)
132 for a in node.attributes.values():
133 if a.namespaceURI == XMLNS_NAMESPACE:
134 # Also add explicit namespaces (for type="xsd:foo", etc)
135 prefix = self.namespaces.ensure_ns(a.localName, a.value)
136 else:
137 new.setAttributeNS(a.namespaceURI, a.name, a.value)
138 for k in node.childNodes:
139 new.appendChild(clone(k, clone))
140 return new
141 new = ns_clone(node, ns_clone)
142 return new
144 def clear_undo(self):
145 # Pop an (op_number, function) off one of these and call the function to
146 # move forwards or backwards in the undo history.
147 self.undo_stack = []
148 self.redo_stack = []
149 self.doing_undo = 0
151 # Each series of suboperations in an undo stack which are part of a single
152 # user op will have the same number...
153 self.user_op = 1
155 #import gc
156 #print "GC freed:", gc.collect()
157 #print "Garbage", gc.garbage
159 def lock(self, node):
160 """Prevent removal of this node (or any ancestor)."""
161 #print "Locking", node.nodeName
162 assert node.nodeType
163 self.locks[node] = self.get_locks(node) + 1
164 if node.parentNode:
165 self.lock(node.parentNode)
167 def unlock(self, node):
168 """Reverse the effect of lock(). Must call unlock the same number
169 of times as lock to fully unlock the node."""
170 l = self.get_locks(node)
171 if l > 1:
172 self.locks[node] = l - 1
173 elif l == 1:
174 del self.locks[node] # Or get a memory leak...
175 if node == self.doc.documentElement:
176 if node.parentNode:
177 self.unlock(node.parentNode)
178 return
179 else:
180 raise Exception('unlock(%s): Node not locked!' % node)
181 if node.parentNode:
182 self.unlock(node.parentNode)
184 def get_locks(self, node):
185 try:
186 return self.locks[node]
187 except KeyError:
188 return 0
190 def lock_and_copy(self, node):
191 """Locks 'node' in the current model and returns a new model
192 with a copy of the subtree."""
193 if self.get_locks(node):
194 raise Exception("Can't enter locked node!")
195 m = Model(self.get_base_uri(node), root_program = self.root_program, do_load = 0)
196 copy = m.import_with_ns(node)
197 root = m.get_root()
198 m.replace_node(root, copy)
199 self.lock(node)
200 return m
202 def mark(self):
203 "Increment the user_op counter. Undo will undo every operation between"
204 "two marks."
205 self.user_op += 1
207 def get_root(self):
208 "Return the true root node (not a view root)"
209 return self.doc.documentElement
211 def add_view(self, view):
212 "'view' provides:"
213 "'update_all(subtree) - called when a major change occurs."
214 #print "New view:", view
215 self.views.append(view)
217 def remove_view(self, view):
218 #print "Removing view", view
219 self.views.remove(view)
220 #print "Now:", self.views
221 # XXX: clears undo on enter!
222 #if not self.views:
223 # self.clear_undo()
225 def update_all(self, node):
226 "Called when 'node' has been updated."
227 "'node' is still in the document, so deleting or replacing"
228 "a node calls this on the parent."
229 for v in self.views:
230 v.update_all(node)
232 def update_replace(self, old, new):
233 "Called when 'old' is replaced by 'new'."
234 for v in self.views:
235 v.update_replace(old, new)
237 def strip_space(self, node = None):
238 if not node:
239 node = self.doc.documentElement
240 def ss(node):
241 if node.nodeType == Node.TEXT_NODE:
242 #node.data = node.data.strip()
243 #if node.data == '':
244 # node.parentNode.removeChild(node)
245 if not node.data.strip():
246 node.parentNode.removeChild(node)
247 else:
248 for k in node.childNodes[:]:
249 ss(k)
250 ss(node)
252 # Changes
254 def normalise(self, node):
255 old = node.cloneNode(1)
256 node.normalize()
257 self.add_undo(lambda: self.replace_node(node, old))
258 self.update_all(node)
260 def convert_to_element(self, node):
261 assert node.nodeType in (Node.COMMENT_NODE, Node.PROCESSING_INSTRUCTION_NODE,
262 Node.TEXT_NODE)
263 new = self.doc.createElementNS(None, node.data.strip())
264 self.replace_node(node, new)
265 return new
267 def convert_to_text(self, node):
268 assert node.nodeType in (Node.COMMENT_NODE, Node.PROCESSING_INSTRUCTION_NODE,
269 Node.TEXT_NODE, Node.ELEMENT_NODE)
270 if node.nodeType == Node.ELEMENT_NODE:
271 new = self.doc.createTextNode(node.localName)
272 else:
273 new = self.doc.createTextNode(node.data)
274 self.replace_node(node, new)
275 return new
277 def convert_to_comment(self, node):
278 assert node.nodeType in (Node.COMMENT_NODE, Node.PROCESSING_INSTRUCTION_NODE,
279 Node.TEXT_NODE)
280 new = self.doc.createComment(node.data)
281 self.replace_node(node, new)
282 return new
284 def remove_ns(self, node):
285 print "remove_ns: Shouldn't need this now!"
286 return
288 nss = GetAllNs(node.parentNode)
289 dns = nss.get(None, None)
290 create = node.ownerDocument.createElementNS
291 # clone is an argument to fix a wierd gc bug in python2.2
292 def ns_clone(node, clone):
293 if node.nodeType != Node.ELEMENT_NODE:
294 return node.cloneNode(1)
295 new = create(dns, node.nodeName)
296 for a in node.attributes.values():
297 if a.localName == 'xmlns' and a.prefix is None:
298 print "Removing xmlns attrib on", node
299 continue
300 new.setAttributeNS(a.namespaceURI, a.name, a.value)
301 for k in node.childNodes:
302 new.appendChild(clone(k, clone))
303 return new
304 new = ns_clone(node, ns_clone)
305 self.replace_node(node, new)
306 return new
308 def set_name(self, node, namespace, name):
309 if self.get_locks(node):
310 raise Exception('Attempt to set name on locked node %s' % node)
312 new = node.ownerDocument.createElementNS(namespace, name)
313 self.replace_shallow(node, new)
314 return new
316 def replace_shallow(self, old, new):
317 """Replace old with new, keeping the old children."""
318 assert not new.childNodes
319 assert not new.parentNode
321 old_name = old.nodeName
322 old_ns = old.namespaceURI
324 kids = old.childNodes[:]
325 attrs = old.attributes.values()
326 parent = old.parentNode
327 [ old.removeChild(k) for k in kids ]
328 parent.replaceChild(new, old)
329 [ new.appendChild(k) for k in kids ]
330 [ new.setAttributeNS(a.namespaceURI, a.name, a.value) for a in attrs ]
332 self.add_undo(lambda: self.replace_shallow(new, old))
334 self.update_replace(old, new)
336 import __main__
337 if __main__.no_gui_mode:
338 def add_undo(self, fn):
339 pass
340 else:
341 def add_undo(self, fn):
342 self.undo_stack.append((self.user_op, fn))
343 if not self.doing_undo:
344 self.redo_stack = []
346 def set_data(self, node, data):
347 old_data = node.data
348 node.data = data
349 self.add_undo(lambda: self.set_data(node, old_data))
350 self.update_all(node)
352 def replace_node(self, old, new):
353 if self.get_locks(old):
354 raise Exception('Attempt to replace locked node %s' % old)
355 old.parentNode.replaceChild(new, old)
356 self.add_undo(lambda: self.replace_node(new, old))
358 self.update_replace(old, new)
360 def delete_shallow(self, node):
361 """Replace node with its contents"""
362 kids = node.childNodes[:]
363 next = node.nextSibling
364 parent = node.parentNode
365 for n in kids + [node]:
366 if self.get_locks(n):
367 raise Exception('Attempt to move/delete locked node %s' % n)
368 for k in kids:
369 self.delete_internal(k)
370 self.delete_internal(node)
371 for k in kids:
372 self.insert_before_interal(next, k, parent)
373 self.update_all(parent)
375 def delete_nodes(self, nodes):
376 #print "Deleting", nodes
377 for n in nodes:
378 if self.get_locks(n):
379 raise Exception('Attempt to delete locked node %s' % n)
380 for n in nodes:
381 p = n.parentNode
382 self.delete_internal(n)
383 self.update_all(p)
385 def delete_internal(self, node):
386 "Doesn't update display."
387 next = node.nextSibling
388 parent = node.parentNode
389 parent.removeChild(node)
390 self.add_undo(lambda: self.insert_before(next, node, parent = parent))
392 def insert_before_interal(self, node, new, parent):
393 "Insert 'new' before 'node'. If 'node' is None then insert at the end"
394 "of parent's children."
395 assert new.nodeType != Node.DOCUMENT_FRAGMENT_NODE
396 #assert parent.nodeType == Node.ELEMENT_NODE
397 parent.insertBefore(new, node)
398 self.add_undo(lambda: self.delete_nodes([new]))
400 def undo(self):
401 if not self.undo_stack:
402 raise Exception('Nothing to undo')
404 assert not self.doing_undo
406 uop = self.undo_stack[-1][0]
408 # Swap stacks so that the undo actions will populate the redo stack...
409 (self.undo_stack, self.redo_stack) = (self.redo_stack, self.undo_stack)
410 self.doing_undo = 1
411 try:
412 while self.redo_stack and self.redo_stack[-1][0] == uop:
413 self.redo_stack[-1][1]()
414 self.redo_stack.pop()
415 finally:
416 (self.undo_stack, self.redo_stack) = (self.redo_stack, self.undo_stack)
417 self.doing_undo = 0
419 def redo(self):
420 if not self.redo_stack:
421 raise Exception('Nothing to redo')
423 uop = self.redo_stack[-1][0]
424 self.doing_undo = 1
425 try:
426 while self.redo_stack and self.redo_stack[-1][0] == uop:
427 self.redo_stack[-1][1]()
428 self.redo_stack.pop()
429 finally:
430 self.doing_undo = 0
432 def insert(self, node, new, index = 0):
433 if len(node.childNodes) > index:
434 self.insert_before(node.childNodes[index], new)
435 else:
436 self.insert_before(None, new, parent = node)
438 def insert_after(self, node, new):
439 self.insert_before(node.nextSibling, new, parent = node.parentNode)
441 def insert_before(self, node, new, parent = None):
442 "Insert 'new' before 'node'. If 'node' is None then insert at the end"
443 "of parent's children. New may be a node, a list or a fragment."
444 if not parent:
445 parent = node.parentNode
446 if type(new) != list:
447 if new.nodeType == Node.DOCUMENT_FRAGMENT_NODE:
448 new = new.childNodes
449 else:
450 new = [new]
451 for n in new: self.insert_before_interal(node, n, parent)
452 self.update_all(parent)
454 def split_qname(self, node, name):
455 if name == 'xmlns':
456 namespaceURI = XMLNS_NAMESPACE
457 localName = name
458 elif ':' in name:
459 prefix, localName = name.split(':')
460 namespaceURI = self.prefix_to_namespace(node, prefix)
461 else:
462 namespaceURI = None
463 localName = name
464 return namespaceURI, localName
466 def set_attrib(self, node, name, value, with_update = 1):
467 """Set an attribute's value. If value is None, remove the attribute.
468 Returns the new attribute node, or None if removing."""
469 namespaceURI, localName = self.split_qname(node, name)
471 if namespaceURI == XMLNS_NAMESPACE:
472 raise Exception("Attempt to set namespace attribute '%s'.\n\n"
473 "Use View->Show Namespaces to edit namespaces." % name)
475 if node.hasAttributeNS(namespaceURI, localName):
476 old = node.getAttributeNS(namespaceURI, localName)
477 else:
478 old = None
479 #print "Set (%s,%s) = %s" % (namespaceURI, name, value)
480 if value != None:
481 node.setAttributeNS(namespaceURI, name, value)
482 else:
483 node.removeAttributeNS(namespaceURI, localName)
485 self.add_undo(lambda: self.set_attrib(node, name, old))
487 if with_update:
488 self.update_all(node)
489 if value != None:
490 if localName == 'xmlns':
491 localName = None
492 return node.attributes[(namespaceURI, localName)]
494 def prefix_to_namespace(self, node, prefix):
495 "Using attributes for namespaces was too confusing. Keep a global list instead."
496 if prefix is None: return None
497 try:
498 return self.namespaces.uri[prefix]
499 except KeyError:
500 raise Exception("Namespace '%s' is not defined. Choose "
501 "View->Show namespaces from the popup menu to set it." % prefix)
503 "Use the xmlns attributes to workout the namespace."
504 nss = GetAllNs(node)
505 if nss.has_key(prefix):
506 return nss[prefix] or None
507 if prefix:
508 if prefix == 'xmlns':
509 return XMLNS_NAMESPACE
510 raise Exception("No such namespace prefix '%s'" % prefix)
511 else:
512 return None
514 def get_base_uri(self, node):
515 """Go up through the parents looking for a uri attribute.
516 If there isn't one, use the document's URI."""
517 while node:
518 if node.nodeType == Node.DOCUMENT_NODE:
519 return self.uri
520 if node.hasAttributeNS(None, 'uri'):
521 return node.getAttributeNS(None, 'uri')
522 node = node.parentNode
523 return None
525 def load_html(self, path):
526 """Load this HTML file and return the new document."""
527 data = file(path).read()
528 data = support.to_html_doc(data)
529 doc = support.parse_data(data, path)
530 self.strip_space(doc)
531 return doc