Remove warning about missing uri.
[dom-editor.git] / Dome / Model.py
blob737f7aede7cffab5651898cd7da5d1f845315d1f
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: continue
134 new.setAttributeNS(a.namespaceURI, a.name, a.value)
135 for k in node.childNodes:
136 new.appendChild(clone(k, clone))
137 return new
138 new = ns_clone(node, ns_clone)
139 return new
141 def clear_undo(self):
142 # Pop an (op_number, function) off one of these and call the function to
143 # move forwards or backwards in the undo history.
144 self.undo_stack = []
145 self.redo_stack = []
146 self.doing_undo = 0
148 # Each series of suboperations in an undo stack which are part of a single
149 # user op will have the same number...
150 self.user_op = 1
152 #import gc
153 #print "GC freed:", gc.collect()
154 #print "Garbage", gc.garbage
156 def lock(self, node):
157 """Prevent removal of this node (or any ancestor)."""
158 #print "Locking", node.nodeName
159 assert node.nodeType
160 self.locks[node] = self.get_locks(node) + 1
161 if node.parentNode:
162 self.lock(node.parentNode)
164 def unlock(self, node):
165 """Reverse the effect of lock(). Must call unlock the same number
166 of times as lock to fully unlock the node."""
167 l = self.get_locks(node)
168 if l > 1:
169 self.locks[node] = l - 1
170 elif l == 1:
171 del self.locks[node] # Or get a memory leak...
172 if node == self.doc.documentElement:
173 if node.parentNode:
174 self.unlock(node.parentNode)
175 return
176 else:
177 raise Exception('unlock(%s): Node not locked!' % node)
178 if node.parentNode:
179 self.unlock(node.parentNode)
181 def get_locks(self, node):
182 try:
183 return self.locks[node]
184 except KeyError:
185 return 0
187 def lock_and_copy(self, node):
188 """Locks 'node' in the current model and returns a new model
189 with a copy of the subtree."""
190 if self.get_locks(node):
191 raise Exception("Can't enter locked node!")
192 m = Model(self.get_base_uri(node), root_program = self.root_program, do_load = 0)
193 copy = m.import_with_ns(node)
194 root = m.get_root()
195 m.replace_node(root, copy)
196 self.lock(node)
197 return m
199 def mark(self):
200 "Increment the user_op counter. Undo will undo every operation between"
201 "two marks."
202 self.user_op += 1
204 def get_root(self):
205 "Return the true root node (not a view root)"
206 return self.doc.documentElement
208 def add_view(self, view):
209 "'view' provides:"
210 "'update_all(subtree) - called when a major change occurs."
211 #print "New view:", view
212 self.views.append(view)
214 def remove_view(self, view):
215 #print "Removing view", view
216 self.views.remove(view)
217 #print "Now:", self.views
218 # XXX: clears undo on enter!
219 #if not self.views:
220 # self.clear_undo()
222 def update_all(self, node):
223 "Called when 'node' has been updated."
224 "'node' is still in the document, so deleting or replacing"
225 "a node calls this on the parent."
226 for v in self.views:
227 v.update_all(node)
229 def update_replace(self, old, new):
230 "Called when 'old' is replaced by 'new'."
231 for v in self.views:
232 v.update_replace(old, new)
234 def strip_space(self, node = None):
235 if not node:
236 node = self.doc.documentElement
237 def ss(node):
238 if node.nodeType == Node.TEXT_NODE:
239 #node.data = node.data.strip()
240 #if node.data == '':
241 # node.parentNode.removeChild(node)
242 if not node.data.strip():
243 node.parentNode.removeChild(node)
244 else:
245 for k in node.childNodes[:]:
246 ss(k)
247 ss(node)
249 # Changes
251 def normalise(self, node):
252 old = node.cloneNode(1)
253 node.normalize()
254 self.add_undo(lambda: self.replace_node(node, old))
255 self.update_all(node)
257 def convert_to_element(self, node):
258 assert node.nodeType in (Node.COMMENT_NODE, Node.PROCESSING_INSTRUCTION_NODE,
259 Node.TEXT_NODE)
260 new = self.doc.createElementNS(None, node.data.strip())
261 self.replace_node(node, new)
262 return new
264 def convert_to_text(self, node):
265 assert node.nodeType in (Node.COMMENT_NODE, Node.PROCESSING_INSTRUCTION_NODE,
266 Node.TEXT_NODE, Node.ELEMENT_NODE)
267 if node.nodeType == Node.ELEMENT_NODE:
268 new = self.doc.createTextNode(node.localName)
269 else:
270 new = self.doc.createTextNode(node.data)
271 self.replace_node(node, new)
272 return new
274 def convert_to_comment(self, node):
275 assert node.nodeType in (Node.COMMENT_NODE, Node.PROCESSING_INSTRUCTION_NODE,
276 Node.TEXT_NODE)
277 new = self.doc.createComment(node.data)
278 self.replace_node(node, new)
279 return new
281 def remove_ns(self, node):
282 print "remove_ns: Shouldn't need this now!"
283 return
285 nss = GetAllNs(node.parentNode)
286 dns = nss.get(None, None)
287 create = node.ownerDocument.createElementNS
288 # clone is an argument to fix a wierd gc bug in python2.2
289 def ns_clone(node, clone):
290 if node.nodeType != Node.ELEMENT_NODE:
291 return node.cloneNode(1)
292 new = create(dns, node.nodeName)
293 for a in node.attributes.values():
294 if a.localName == 'xmlns' and a.prefix is None:
295 print "Removing xmlns attrib on", node
296 continue
297 new.setAttributeNS(a.namespaceURI, a.name, a.value)
298 for k in node.childNodes:
299 new.appendChild(clone(k, clone))
300 return new
301 new = ns_clone(node, ns_clone)
302 self.replace_node(node, new)
303 return new
305 def set_name(self, node, namespace, name):
306 if self.get_locks(node):
307 raise Exception('Attempt to set name on locked node %s' % node)
309 new = node.ownerDocument.createElementNS(namespace, name)
310 self.replace_shallow(node, new)
311 return new
313 def replace_shallow(self, old, new):
314 """Replace old with new, keeping the old children."""
315 assert not new.childNodes
316 assert not new.parentNode
318 old_name = old.nodeName
319 old_ns = old.namespaceURI
321 kids = old.childNodes[:]
322 attrs = old.attributes.values()
323 parent = old.parentNode
324 [ old.removeChild(k) for k in kids ]
325 parent.replaceChild(new, old)
326 [ new.appendChild(k) for k in kids ]
327 [ new.setAttributeNS(a.namespaceURI, a.name, a.value) for a in attrs ]
329 self.add_undo(lambda: self.replace_shallow(new, old))
331 self.update_replace(old, new)
333 import __main__
334 if __main__.no_gui_mode:
335 def add_undo(self, fn):
336 pass
337 else:
338 def add_undo(self, fn):
339 self.undo_stack.append((self.user_op, fn))
340 if not self.doing_undo:
341 self.redo_stack = []
343 def set_data(self, node, data):
344 old_data = node.data
345 node.data = data
346 self.add_undo(lambda: self.set_data(node, old_data))
347 self.update_all(node)
349 def replace_node(self, old, new):
350 if self.get_locks(old):
351 raise Exception('Attempt to replace locked node %s' % old)
352 old.parentNode.replaceChild(new, old)
353 self.add_undo(lambda: self.replace_node(new, old))
355 self.update_replace(old, new)
357 def delete_shallow(self, node):
358 """Replace node with its contents"""
359 kids = node.childNodes[:]
360 next = node.nextSibling
361 parent = node.parentNode
362 for n in kids + [node]:
363 if self.get_locks(n):
364 raise Exception('Attempt to move/delete locked node %s' % n)
365 for k in kids:
366 self.delete_internal(k)
367 self.delete_internal(node)
368 for k in kids:
369 self.insert_before_interal(next, k, parent)
370 self.update_all(parent)
372 def delete_nodes(self, nodes):
373 #print "Deleting", nodes
374 for n in nodes:
375 if self.get_locks(n):
376 raise Exception('Attempt to delete locked node %s' % n)
377 for n in nodes:
378 p = n.parentNode
379 self.delete_internal(n)
380 self.update_all(p)
382 def delete_internal(self, node):
383 "Doesn't update display."
384 next = node.nextSibling
385 parent = node.parentNode
386 parent.removeChild(node)
387 self.add_undo(lambda: self.insert_before(next, node, parent = parent))
389 def insert_before_interal(self, node, new, parent):
390 "Insert 'new' before 'node'. If 'node' is None then insert at the end"
391 "of parent's children."
392 assert new.nodeType != Node.DOCUMENT_FRAGMENT_NODE
393 #assert parent.nodeType == Node.ELEMENT_NODE
394 parent.insertBefore(new, node)
395 self.add_undo(lambda: self.delete_nodes([new]))
397 def undo(self):
398 if not self.undo_stack:
399 raise Exception('Nothing to undo')
401 assert not self.doing_undo
403 uop = self.undo_stack[-1][0]
405 # Swap stacks so that the undo actions will populate the redo stack...
406 (self.undo_stack, self.redo_stack) = (self.redo_stack, self.undo_stack)
407 self.doing_undo = 1
408 try:
409 while self.redo_stack and self.redo_stack[-1][0] == uop:
410 self.redo_stack[-1][1]()
411 self.redo_stack.pop()
412 finally:
413 (self.undo_stack, self.redo_stack) = (self.redo_stack, self.undo_stack)
414 self.doing_undo = 0
416 def redo(self):
417 if not self.redo_stack:
418 raise Exception('Nothing to redo')
420 uop = self.redo_stack[-1][0]
421 self.doing_undo = 1
422 try:
423 while self.redo_stack and self.redo_stack[-1][0] == uop:
424 self.redo_stack[-1][1]()
425 self.redo_stack.pop()
426 finally:
427 self.doing_undo = 0
429 def insert(self, node, new, index = 0):
430 if len(node.childNodes) > index:
431 self.insert_before(node.childNodes[index], new)
432 else:
433 self.insert_before(None, new, parent = node)
435 def insert_after(self, node, new):
436 self.insert_before(node.nextSibling, new, parent = node.parentNode)
438 def insert_before(self, node, new, parent = None):
439 "Insert 'new' before 'node'. If 'node' is None then insert at the end"
440 "of parent's children. New may be a node, a list or a fragment."
441 if not parent:
442 parent = node.parentNode
443 if type(new) != list:
444 if new.nodeType == Node.DOCUMENT_FRAGMENT_NODE:
445 new = new.childNodes
446 else:
447 new = [new]
448 for n in new: self.insert_before_interal(node, n, parent)
449 self.update_all(parent)
451 def split_qname(self, node, name):
452 if name == 'xmlns':
453 namespaceURI = XMLNS_NAMESPACE
454 localName = name
455 elif ':' in name:
456 prefix, localName = name.split(':')
457 namespaceURI = self.prefix_to_namespace(node, prefix)
458 else:
459 namespaceURI = None
460 localName = name
461 return namespaceURI, localName
463 def set_attrib(self, node, name, value, with_update = 1):
464 """Set an attribute's value. If value is None, remove the attribute.
465 Returns the new attribute node, or None if removing."""
466 namespaceURI, localName = self.split_qname(node, name)
468 if namespaceURI == XMLNS_NAMESPACE:
469 raise Exception("Attempt to set namespace attribute '%s'.\n\n"
470 "Use View->Show Namespaces to edit namespaces." % name)
472 if node.hasAttributeNS(namespaceURI, localName):
473 old = node.getAttributeNS(namespaceURI, localName)
474 else:
475 old = None
476 #print "Set (%s,%s) = %s" % (namespaceURI, name, value)
477 if value != None:
478 node.setAttributeNS(namespaceURI, name, value)
479 else:
480 node.removeAttributeNS(namespaceURI, localName)
482 self.add_undo(lambda: self.set_attrib(node, name, old))
484 if with_update:
485 self.update_all(node)
486 if value != None:
487 if localName == 'xmlns':
488 localName = None
489 return node.attributes[(namespaceURI, localName)]
491 def prefix_to_namespace(self, node, prefix):
492 "Using attributes for namespaces was too confusing. Keep a global list instead."
493 if prefix is None: return None
494 try:
495 return self.namespaces.uri[prefix]
496 except KeyError:
497 raise Exception("Namespace '%s' is not defined. Choose "
498 "View->Show namespaces from the popup menu to set it." % prefix)
500 "Use the xmlns attributes to workout the namespace."
501 nss = GetAllNs(node)
502 if nss.has_key(prefix):
503 return nss[prefix] or None
504 if prefix:
505 if prefix == 'xmlns':
506 return XMLNS_NAMESPACE
507 raise Exception("No such namespace prefix '%s'" % prefix)
508 else:
509 return None
511 def get_base_uri(self, node):
512 """Go up through the parents looking for a uri attribute.
513 If there isn't one, use the document's URI."""
514 while node:
515 if node.nodeType == Node.DOCUMENT_NODE:
516 return self.uri
517 if node.hasAttributeNS(None, 'uri'):
518 return node.getAttributeNS(None, 'uri')
519 node = node.parentNode
520 return None
522 def load_html(self, path):
523 """Load this HTML file and return the new document."""
524 data = file(path).read()
525 data = support.to_html_doc(data)
526 doc = support.parse_data(data, path)
527 self.strip_space(doc)
528 return doc