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