Slightly more efficient stripping, and updates for pygtk.
[dom-editor.git] / Dome / Model.py
blobca1952bb78f0543226b40f9cdb2707299618cac0
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, do_load = 1):
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 do_load and (path.endswith('.html') or path.endswith('.htm')):
42 doc = self.load_html(path)
43 if not doc and not root_program:
44 from Ft.Xml.InputSource import InputSourceFactory
45 isrc = InputSourceFactory()
46 doc = nonvalParse(isrc.fromUri(path))
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 import constants
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 == 'dome-program':
60 self.root_program = load_dome_program(x)
61 elif x.localName == 'dome-data':
62 for y in x.childNodes:
63 if y.nodeType == Node.ELEMENT_NODE:
64 data_to_load = y
65 if dome_data:
66 data_to_load = dome_data.documentElement
67 elif (root.namespaceURI == constants.XSLT_NS and
68 root.localName in ['stylesheet', 'transform']) or \
69 root.hasAttributeNS(constants.XSLT_NS, 'version'):
70 import xslt
71 self.root_program = xslt.import_sheet(doc)
72 x = implementation.createDocument(None, 'xslt', None)
73 data_to_load = x.documentElement
74 src = get_xslt_source(x, dome_data)
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 = support.import_with_ns(self.doc, 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 clear_undo(self):
98 # Pop an (op_number, function) off one of these and call the function to
99 # move forwards or backwards in the undo history.
100 self.undo_stack = []
101 self.redo_stack = []
102 self.doing_undo = 0
104 # Each series of suboperations in an undo stack which are part of a single
105 # user op will have the same number...
106 self.user_op = 1
108 #import gc
109 #print "GC freed:", gc.collect()
110 #print "Garbage", gc.garbage
112 def lock(self, node):
113 """Prevent removal of this node (or any ancestor)."""
114 #print "Locking", node.nodeName
115 self.locks[node] = self.get_locks(node) + 1
116 if node.parentNode:
117 self.lock(node.parentNode)
119 def unlock(self, node):
120 """Reverse the effect of lock(). Must call unlock the same number
121 of times as lock to fully unlock the node."""
122 l = self.get_locks(node)
123 if l > 1:
124 self.locks[node] = l - 1
125 elif l == 1:
126 del self.locks[node] # Or get a memory leak...
127 if node == self.doc.documentElement:
128 if node.parentNode:
129 self.unlock(node.parentNode)
130 return
131 else:
132 raise Exception('unlock(%s): Node not locked!' % node)
133 if node.parentNode:
134 self.unlock(node.parentNode)
136 def get_locks(self, node):
137 try:
138 return self.locks[node]
139 except KeyError:
140 return 0
142 def lock_and_copy(self, node):
143 """Locks 'node' in the current model and returns a new model
144 with a copy of the subtree."""
145 if self.get_locks(node):
146 raise Exception("Can't enter locked node!")
147 m = Model(self.get_base_uri(node), root_program = self.root_program, do_load = 0)
148 copy = support.import_with_ns(m.doc, node)
149 root = m.get_root()
150 m.replace_node(root, copy)
151 self.lock(node)
152 return m
154 def mark(self):
155 "Increment the user_op counter. Undo will undo every operation between"
156 "two marks."
157 self.user_op += 1
159 def get_root(self):
160 "Return the true root node (not a view root)"
161 return self.doc.documentElement
163 def add_view(self, view):
164 "'view' provides:"
165 "'update_all(subtree) - called when a major change occurs."
166 #print "New view:", view
167 self.views.append(view)
169 def remove_view(self, view):
170 #print "Removing view", view
171 self.views.remove(view)
172 #print "Now:", self.views
173 if not self.views:
174 self.clear_undo()
176 def update_all(self, node):
177 "Called when 'node' has been updated."
178 "'node' is still in the document, so deleting or replacing"
179 "a node calls this on the parent."
180 for v in self.views:
181 v.update_all(node)
183 def update_replace(self, old, new):
184 "Called when 'old' is replaced by 'new'."
185 for v in self.views:
186 v.update_replace(old, new)
188 def strip_space(self, node = None):
189 if not node:
190 node = self.doc.documentElement
191 def ss(node):
192 if node.nodeType == Node.TEXT_NODE:
193 #node.data = node.data.strip()
194 #if node.data == '':
195 # node.parentNode.removeChild(node)
196 if not node.data.strip():
197 node.parentNode.removeChild(node)
198 else:
199 for k in node.childNodes[:]:
200 ss(k)
201 ss(node)
203 # Changes
205 def normalise(self, node):
206 old = node.cloneNode(1)
207 node.normalize()
208 self.add_undo(lambda: self.replace_node(node, old))
209 self.update_all(node)
211 def convert_to_text(self, node):
212 assert node.nodeType in (Node.COMMENT_NODE, Node.PROCESSING_INSTRUCTION_NODE,
213 Node.TEXT_NODE)
214 new = self.doc.createTextNode(node.data)
215 self.replace_node(node, new)
216 return new
218 def convert_to_comment(self, node):
219 assert node.nodeType in (Node.COMMENT_NODE, Node.PROCESSING_INSTRUCTION_NODE,
220 Node.TEXT_NODE)
221 new = self.doc.createComment(node.data)
222 self.replace_node(node, new)
223 return new
225 def remove_ns(self, node):
226 nss = GetAllNs(node.parentNode)
227 dns = nss.get(None, None)
228 create = node.ownerDocument.createElementNS
229 # clone is an argument to fix a wierd gc bug in python2.2
230 def ns_clone(node, clone):
231 if node.nodeType != Node.ELEMENT_NODE:
232 return node.cloneNode(1)
233 new = create(dns, node.nodeName)
234 for a in node.attributes.values():
235 if a.localName == 'xmlns' and a.prefix is None:
236 print "Removing xmlns attrib on", node
237 continue
238 new.setAttributeNS(a.namespaceURI, a.name, a.value)
239 for k in node.childNodes:
240 new.appendChild(clone(k, clone))
241 return new
242 new = ns_clone(node, ns_clone)
243 self.replace_node(node, new)
244 return new
246 def set_name(self, node, namespace, name):
247 if self.get_locks(node):
248 raise Exception('Attempt to set name on locked node %s' % node)
250 new = node.ownerDocument.createElementNS(namespace, name)
251 self.replace_shallow(node, new)
252 return new
254 def replace_shallow(self, old, new):
255 """Replace old with new, keeping the old children."""
256 assert not new.childNodes
257 assert not new.parentNode
259 old_name = old.nodeName
260 old_ns = old.namespaceURI
262 kids = old.childNodes[:]
263 attrs = old.attributes.values()
264 parent = old.parentNode
265 [ old.removeChild(k) for k in kids ]
266 parent.replaceChild(new, old)
267 [ new.appendChild(k) for k in kids ]
268 [ new.setAttributeNS(a.namespaceURI, a.name, a.value) for a in attrs ]
270 self.add_undo(lambda: self.replace_shallow(new, old))
272 self.update_replace(old, new)
274 import __main__
275 if __main__.no_gui_mode:
276 def add_undo(self, fn):
277 pass
278 else:
279 def add_undo(self, fn):
280 self.undo_stack.append((self.user_op, fn))
281 if not self.doing_undo:
282 self.redo_stack = []
284 def set_data(self, node, data):
285 old_data = node.data
286 node.data = data
287 self.add_undo(lambda: self.set_data(node, old_data))
288 self.update_all(node)
290 def replace_node(self, old, new):
291 if self.get_locks(old):
292 raise Exception('Attempt to replace locked node %s' % old)
293 old.parentNode.replaceChild(new, old)
294 self.add_undo(lambda: self.replace_node(new, old))
296 self.update_replace(old, new)
298 def delete_shallow(self, node):
299 """Replace node with its contents"""
300 kids = node.childNodes[:]
301 next = node.nextSibling
302 parent = node.parentNode
303 for n in kids + [node]:
304 if self.get_locks(n):
305 raise Exception('Attempt to move/delete locked node %s' % n)
306 for k in kids:
307 self.delete_internal(k)
308 self.delete_internal(node)
309 for k in kids:
310 self.insert_before_interal(next, k, parent)
311 self.update_all(parent)
313 def delete_nodes(self, nodes):
314 #print "Deleting", nodes
315 for n in nodes:
316 if self.get_locks(n):
317 raise Exception('Attempt to delete locked node %s' % n)
318 for n in nodes:
319 p = n.parentNode
320 self.delete_internal(n)
321 self.update_all(p)
323 def delete_internal(self, node):
324 "Doesn't update display."
325 next = node.nextSibling
326 parent = node.parentNode
327 parent.removeChild(node)
328 self.add_undo(lambda: self.insert_before(next, node, parent = parent))
330 def insert_before_interal(self, node, new, parent):
331 "Insert 'new' before 'node'. If 'node' is None then insert at the end"
332 "of parent's children."
333 assert new.nodeType != Node.DOCUMENT_FRAGMENT_NODE
334 assert parent.nodeType == Node.ELEMENT_NODE
335 parent.insertBefore(new, node)
336 self.add_undo(lambda: self.delete_nodes([new]))
338 def undo(self):
339 if not self.undo_stack:
340 raise Exception('Nothing to undo')
342 assert not self.doing_undo
344 uop = self.undo_stack[-1][0]
346 # Swap stacks so that the undo actions will populate the redo stack...
347 (self.undo_stack, self.redo_stack) = (self.redo_stack, self.undo_stack)
348 self.doing_undo = 1
349 try:
350 while self.redo_stack and self.redo_stack[-1][0] == uop:
351 self.redo_stack[-1][1]()
352 self.redo_stack.pop()
353 finally:
354 (self.undo_stack, self.redo_stack) = (self.redo_stack, self.undo_stack)
355 self.doing_undo = 0
357 def redo(self):
358 if not self.redo_stack:
359 raise Exception('Nothing to redo')
361 uop = self.redo_stack[-1][0]
362 self.doing_undo = 1
363 try:
364 while self.redo_stack and self.redo_stack[-1][0] == uop:
365 self.redo_stack[-1][1]()
366 self.redo_stack.pop()
367 finally:
368 self.doing_undo = 0
370 def insert(self, node, new, index = 0):
371 if len(node.childNodes) > index:
372 self.insert_before(node.childNodes[index], new)
373 else:
374 self.insert_before(None, new, parent = node)
376 def insert_after(self, node, new):
377 self.insert_before(node.nextSibling, new, parent = node.parentNode)
379 def insert_before(self, node, new, parent = None):
380 "Insert 'new' before 'node'. If 'node' is None then insert at the end"
381 "of parent's children."
382 if not parent:
383 parent = node.parentNode
384 if new.nodeType == Node.DOCUMENT_FRAGMENT_NODE:
385 for n in new.childNodes[:]:
386 self.insert_before_interal(node, n, parent)
387 else:
388 self.insert_before_interal(node, new, parent)
389 self.update_all(parent)
391 def split_qname(self, node, name):
392 if name == 'xmlns':
393 namespaceURI = XMLNS_NAMESPACE
394 localName = name
395 elif ':' in name:
396 prefix, localName = name.split(':')
397 namespaceURI = self.prefix_to_namespace(node, prefix)
398 else:
399 namespaceURI = None
400 localName = name
401 return namespaceURI, localName
403 def set_attrib(self, node, name, value, with_update = 1):
404 """Set an attribute's value. If value is None, remove the attribute.
405 Returns the new attribute node, or None if removing."""
406 namespaceURI, localName = self.split_qname(node, name)
408 if node.hasAttributeNS(namespaceURI, localName):
409 old = node.getAttributeNS(namespaceURI, localName)
410 else:
411 old = None
412 #print "Set (%s,%s) = %s" % (namespaceURI, name, value)
413 if value != None:
414 node.setAttributeNS(namespaceURI, name, value)
415 else:
416 node.removeAttributeNS(namespaceURI, localName)
418 self.add_undo(lambda: self.set_attrib(node, name, old))
420 if with_update:
421 self.update_all(node)
422 if value != None:
423 if localName == 'xmlns':
424 localName = None
425 return node.attributes[(namespaceURI, localName)]
427 def prefix_to_namespace(self, node, prefix):
428 "Use the xmlns attributes to workout the namespace."
429 nss = GetAllNs(node)
430 if nss.has_key(prefix):
431 return nss[prefix] or None
432 if prefix:
433 if prefix == 'xmlns':
434 return XMLNS_NAMESPACE
435 raise Exception("No such namespace prefix '%s'" % prefix)
436 else:
437 return None
439 def get_base_uri(self, node):
440 """Go up through the parents looking for a uri attribute.
441 If there isn't one, use the document's URI."""
442 while node:
443 if node.nodeType == Node.DOCUMENT_NODE:
444 return self.uri
445 if node.hasAttributeNS(None, 'uri'):
446 return node.getAttributeNS(None, 'uri')
447 node = node.parentNode
448 return None
450 def load_html(self, path):
451 """Load this HTML file and return the new document."""
452 data = file(path).read()
453 data = support.to_html_doc(data)
454 doc = support.parse_data(data, path)
455 self.strip_space(doc)
456 return doc