Fix relative URI handling.
[dom-editor.git] / Dome / Model.py
blob3e0eb0e9b0f0344bdca6b0d6f7e942a3ace966c1
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 def get_xslt_source(doc, dome_data):
19 print "get_xslt_source", dome_data
20 src = doc.createElementNS(None, 'Source')
21 if file:
22 src.appendChild(support.import_with_ns(doc, dome_data.documentElement))
23 return src
25 class Model:
26 def __init__(self, path, root_program = None, dome_data = None):
27 "If root_program is given, then no data is loaded (used for lock_and_copy)."
28 self.uri = 'Prog.dome'
30 if dome_data:
31 from Ft.Xml.InputSource import InputSourceFactory
32 isrc = InputSourceFactory()
33 dome_data = nonvalParse(isrc.fromUri(dome_data))
35 self.clear_undo()
37 doc = None
38 if path:
39 if path != '-':
40 self.uri = path
41 if not root_program:
42 from Ft.Xml.InputSource import InputSourceFactory
43 isrc = InputSourceFactory()
44 doc = nonvalParse(isrc.fromUri(path))
45 if not doc:
46 doc = implementation.createDocument(None, 'root', None)
47 root = doc.documentElement
49 self.root_program = None
50 data_to_load = None
52 from Program import Program, load_dome_program
53 import constants
54 if root.namespaceURI == constants.DOME_NS and root.localName == 'dome':
55 for x in root.childNodes:
56 if x.namespaceURI == constants.DOME_NS:
57 if x.localName == 'dome-program':
58 self.root_program = load_dome_program(x)
59 elif x.localName == 'dome-data':
60 for y in x.childNodes:
61 if y.nodeType == Node.ELEMENT_NODE:
62 data_to_load = y
63 if dome_data:
64 data_to_load = dome_data.documentElement
65 elif (root.namespaceURI == constants.XSLT_NS and
66 root.localName in ['stylesheet', 'transform']) or \
67 root.hasAttributeNS(constants.XSLT_NS, 'version'):
68 import xslt
69 self.root_program = xslt.import_sheet(doc)
70 x = implementation.createDocument(None, 'xslt', None)
71 data_to_load = x.documentElement
72 src = get_xslt_source(x, dome_data)
73 data_to_load.appendChild(x.createElementNS(None, 'Result'))
74 data_to_load.appendChild(src)
75 dome_data = None
76 else:
77 data_to_load = root
79 if root_program:
80 self.root_program = root_program
81 else:
82 if not self.root_program:
83 self.root_program = Program('Root')
85 if data_to_load:
86 self.doc = implementation.createDocument(None, 'root', None)
87 if not root_program:
88 node = support.import_with_ns(self.doc, data_to_load)
89 self.doc.replaceChild(node, self.doc.documentElement)
90 self.strip_space()
92 self.views = [] # Notified when something changes
93 self.locks = {} # Node -> number of locks
95 def clear_undo(self):
96 # Pop an (op_number, function) off one of these and call the function to
97 # move forwards or backwards in the undo history.
98 self.undo_stack = []
99 self.redo_stack = []
100 self.doing_undo = 0
102 # Each series of suboperations in an undo stack which are part of a single
103 # user op will have the same number...
104 self.user_op = 1
106 #import gc
107 #print "GC freed:", gc.collect()
108 #print "Garbage", gc.garbage
110 def lock(self, node):
111 """Prevent removal of this node (or any ancestor)."""
112 #print "Locking", node.nodeName
113 self.locks[node] = self.get_locks(node) + 1
114 if node.parentNode:
115 self.lock(node.parentNode)
117 def unlock(self, node):
118 """Reverse the effect of lock(). Must call unlock the same number
119 of times as lock to fully unlock the node."""
120 l = self.get_locks(node)
121 if l > 1:
122 self.locks[node] = l - 1
123 elif l == 1:
124 del self.locks[node] # Or get a memory leak...
125 if node == self.doc.documentElement:
126 if node.parentNode:
127 self.unlock(node.parentNode)
128 return
129 else:
130 raise Exception('unlock(%s): Node not locked!' % node)
131 if node.parentNode:
132 self.unlock(node.parentNode)
134 def get_locks(self, node):
135 try:
136 return self.locks[node]
137 except KeyError:
138 return 0
140 def lock_and_copy(self, node):
141 """Locks 'node' in the current model and returns a new model
142 with a copy of the subtree."""
143 if self.get_locks(node):
144 raise Exception("Can't enter locked node!")
145 m = Model(self.get_base_uri(node), root_program = self.root_program)
146 copy = support.import_with_ns(m.doc, node)
147 root = m.get_root()
148 m.replace_node(root, copy)
149 self.lock(node)
150 return m
152 def mark(self):
153 "Increment the user_op counter. Undo will undo every operation between"
154 "two marks."
155 self.user_op += 1
157 def get_root(self):
158 "Return the true root node (not a view root)"
159 return self.doc.documentElement
161 def add_view(self, view):
162 "'view' provides:"
163 "'update_all(subtree) - called when a major change occurs."
164 #print "New view:", view
165 self.views.append(view)
167 def remove_view(self, view):
168 #print "Removing view", view
169 self.views.remove(view)
170 #print "Now:", self.views
171 if not self.views:
172 self.clear_undo()
174 def update_all(self, node):
175 "Called when 'node' has been updated."
176 "'node' is still in the document, so deleting or replacing"
177 "a node calls this on the parent."
178 for v in self.views:
179 v.update_all(node)
181 def update_replace(self, old, new):
182 "Called when 'old' is replaced by 'new'."
183 for v in self.views:
184 v.update_replace(old, new)
186 def strip_space(self, node = None):
187 if not node:
188 node = self.doc.documentElement
189 if node.nodeType == Node.TEXT_NODE:
190 #node.data = node.data.strip()
191 #if node.data == '':
192 # node.parentNode.removeChild(node)
193 if not node.data.strip():
194 node.parentNode.removeChild(node)
195 else:
196 for k in node.childNodes[:]:
197 self.strip_space(k)
199 # Changes
201 def normalise(self, node):
202 old = node.cloneNode(1)
203 node.normalize()
204 self.add_undo(lambda: self.replace_node(node, old))
205 self.update_all(node)
207 def convert_to_text(self, node):
208 assert node.nodeType == Node.COMMENT_NODE
209 new = self.doc.createTextNode(node.data)
210 self.replace_node(node, new)
211 return new
213 def remove_ns(self, node):
214 nss = GetAllNs(node.parentNode)
215 dns = nss.get(None, None)
216 create = node.ownerDocument.createElementNS
217 def ns_clone(node):
218 if node.nodeType != Node.ELEMENT_NODE:
219 return node.cloneNode(1)
220 new = create(dns, node.nodeName)
221 for a in node.attributes.values():
222 if a.localName == 'xmlns' and a.prefix is None:
223 print "Removing xmlns attrib on", node
224 continue
225 new.setAttributeNS(a.namespaceURI, a.name, a.value)
226 #print "Now", new
227 for k in node.childNodes:
228 new.appendChild(ns_clone(k))
229 return new
230 new = ns_clone(node)
231 self.replace_node(node, new)
232 return new
234 def set_name(self, node, namespace, name):
235 if self.get_locks(node):
236 raise Exception('Attempt to set name on locked node %s' % node)
238 new = node.ownerDocument.createElementNS(namespace, name)
239 self.replace_shallow(node, new)
240 return new
242 def replace_shallow(self, old, new):
243 """Replace old with new, keeping the old children."""
244 assert not new.childNodes
245 assert not new.parentNode
247 old_name = old.nodeName
248 old_ns = old.namespaceURI
250 kids = old.childNodes[:]
251 attrs = old.attributes.values()
252 parent = old.parentNode
253 [ old.removeChild(k) for k in kids ]
254 parent.replaceChild(new, old)
255 [ new.appendChild(k) for k in kids ]
256 [ new.setAttributeNS(a.namespaceURI, a.name, a.value) for a in attrs ]
258 self.add_undo(lambda: self.replace_shallow(new, old))
260 self.update_replace(old, new)
262 import __main__
263 if __main__.no_gui_mode:
264 def add_undo(self, fn):
265 pass
266 else:
267 def add_undo(self, fn):
268 self.undo_stack.append((self.user_op, fn))
269 if not self.doing_undo:
270 self.redo_stack = []
272 def set_data(self, node, data):
273 old_data = node.data
274 node.data = data
275 self.add_undo(lambda: self.set_data(node, old_data))
276 self.update_all(node)
278 def replace_node(self, old, new):
279 if self.get_locks(old):
280 raise Exception('Attempt to replace locked node %s' % old)
281 old.parentNode.replaceChild(new, old)
282 self.add_undo(lambda: self.replace_node(new, old))
284 self.update_replace(old, new)
286 def delete_shallow(self, node):
287 """Replace node with its contents"""
288 kids = node.childNodes[:]
289 next = node.nextSibling
290 parent = node.parentNode
291 for n in kids + [node]:
292 if self.get_locks(n):
293 raise Exception('Attempt to move/delete locked node %s' % n)
294 for k in kids:
295 self.delete_internal(k)
296 self.delete_internal(node)
297 for k in kids:
298 self.insert_before_interal(next, k, parent)
299 self.update_all(parent)
301 def delete_nodes(self, nodes):
302 #print "Deleting", nodes
303 for n in nodes:
304 if self.get_locks(n):
305 raise Exception('Attempt to delete locked node %s' % n)
306 for n in nodes:
307 p = n.parentNode
308 self.delete_internal(n)
309 self.update_all(p)
311 def delete_internal(self, node):
312 "Doesn't update display."
313 next = node.nextSibling
314 parent = node.parentNode
315 parent.removeChild(node)
316 self.add_undo(lambda: self.insert_before(next, node, parent = parent))
318 def insert_before_interal(self, node, new, parent):
319 "Insert 'new' before 'node'. If 'node' is None then insert at the end"
320 "of parent's children."
321 assert new.nodeType != Node.DOCUMENT_FRAGMENT_NODE
322 assert parent.nodeType == Node.ELEMENT_NODE
323 parent.insertBefore(new, node)
324 self.add_undo(lambda: self.delete_nodes([new]))
326 def undo(self):
327 if not self.undo_stack:
328 raise Exception('Nothing to undo')
330 assert not self.doing_undo
332 uop = self.undo_stack[-1][0]
334 # Swap stacks so that the undo actions will populate the redo stack...
335 (self.undo_stack, self.redo_stack) = (self.redo_stack, self.undo_stack)
336 self.doing_undo = 1
337 try:
338 while self.redo_stack and self.redo_stack[-1][0] == uop:
339 self.redo_stack[-1][1]()
340 self.redo_stack.pop()
341 finally:
342 (self.undo_stack, self.redo_stack) = (self.redo_stack, self.undo_stack)
343 self.doing_undo = 0
345 def redo(self):
346 if not self.redo_stack:
347 raise Exception('Nothing to redo')
349 uop = self.redo_stack[-1][0]
350 self.doing_undo = 1
351 try:
352 while self.redo_stack and self.redo_stack[-1][0] == uop:
353 self.redo_stack[-1][1]()
354 self.redo_stack.pop()
355 finally:
356 self.doing_undo = 0
358 def insert(self, node, new, index = 0):
359 if len(node.childNodes) > index:
360 self.insert_before(node.childNodes[index], new)
361 else:
362 self.insert_before(None, new, parent = node)
364 def insert_after(self, node, new):
365 self.insert_before(node.nextSibling, new, parent = node.parentNode)
367 def insert_before(self, node, new, parent = None):
368 "Insert 'new' before 'node'. If 'node' is None then insert at the end"
369 "of parent's children."
370 if not parent:
371 parent = node.parentNode
372 if new.nodeType == Node.DOCUMENT_FRAGMENT_NODE:
373 for n in new.childNodes[:]:
374 self.insert_before_interal(node, n, parent)
375 else:
376 self.insert_before_interal(node, new, parent)
377 self.update_all(parent)
379 def split_qname(self, node, name):
380 if name == 'xmlns':
381 namespaceURI = XMLNS_NAMESPACE
382 localName = name
383 elif ':' in name:
384 prefix, localName = name.split(':')
385 namespaceURI = self.prefix_to_namespace(node, prefix)
386 else:
387 namespaceURI = None
388 localName = name
389 return namespaceURI, localName
391 def set_attrib(self, node, name, value, with_update = 1):
392 """Set an attribute's value. If value is None, remove the attribute.
393 Returns the new attribute node, or None if removing."""
394 namespaceURI, localName = self.split_qname(node, name)
396 if node.hasAttributeNS(namespaceURI, localName):
397 old = node.getAttributeNS(namespaceURI, localName)
398 else:
399 old = None
400 #print "Set (%s,%s) = %s" % (namespaceURI, name, value)
401 if value != None:
402 node.setAttributeNS(namespaceURI, name, value)
403 else:
404 node.removeAttributeNS(namespaceURI, localName)
406 self.add_undo(lambda: self.set_attrib(node, name, old))
408 if with_update:
409 self.update_all(node)
410 if value != None:
411 if localName == 'xmlns':
412 localName = None
413 return node.attributes[(namespaceURI, localName)]
415 def prefix_to_namespace(self, node, prefix):
416 "Use the xmlns attributes to workout the namespace."
417 nss = GetAllNs(node)
418 if nss.has_key(prefix):
419 return nss[prefix] or None
420 if prefix:
421 if prefix == 'xmlns':
422 return XMLNS_NAMESPACE
423 raise Exception("No such namespace prefix '%s'" % prefix)
424 else:
425 return None
427 def get_base_uri(self, node):
428 """Go up through the parents looking for a uri attribute.
429 If there isn't one, use the document's URI."""
430 while node:
431 if node.nodeType == Node.DOCUMENT_NODE:
432 return self.uri
433 if node.hasAttributeNS(None, 'uri'):
434 return node.getAttributeNS(None, 'uri')
435 node = node.parentNode
436 return None