Select attribute after subst.
[dom-editor.git] / Dome / View.py
blob79e46c073ed2e43bb4d2ad388bc393f21b579bb3
1 from __future__ import nested_scopes
3 import GDK
4 from support import *
5 from rox import support
6 from xml.dom import Node, ext, XMLNS_NAMESPACE
7 from Ft.Xml import XPath
8 from Ft.Xml.XPath import FT_EXT_NAMESPACE, Context
9 from xml.dom.ext.reader import PyExpat
10 from Ft.Xml.cDomlette import implementation
12 import os, re, string, types
13 import urlparse
14 import Html
15 from StringIO import StringIO
17 from Program import Op
18 from Beep import Beep
20 import time
21 import urllib
22 import traceback
24 from constants import *
26 def elements(node):
27 out = []
28 for x in node.childNodes:
29 if x.nodeType == Node.ELEMENT_NODE:
30 out.append(x)
31 return out
33 normal_chars = string.letters + string.digits + "-"
35 fast_global = re.compile('//([-A-Za-z][-A-Za-z0-9]*:)?[-A-Za-z][-A-Za-z0-9]*$')
37 # An view contains:
38 # - A ref to a DOM document
39 # - A set of current nodes
40 # - A root node
41 # - A chroot stack
42 # It does not have any display code. It does contain code to perform actions
43 # (actions affect the document AND the view state).
45 # These actions can be repeated using '.'
46 record_again = [
47 "do_global",
48 "subst",
49 "python",
50 "ask",
51 "yank",
52 "shallow_yank",
53 "delete_node",
54 "delete_node_no_clipboard",
55 "delete_shallow",
56 "play",
57 "map",
58 "change_node",
59 "add_node",
60 "suck",
61 "http_post",
62 "put_before",
63 "put_after",
64 "put_replace",
65 "put_as_child",
66 "yank_value",
67 "yank_attribs",
68 "paste_attribs",
69 "compare",
70 "fail",
71 "attribute",
72 "set_attrib",
73 "add_attrib",
74 "soap_send",
75 "show_canvas",
76 "show_html",
77 "select_dups",
78 "select_region",
81 def same(a, b):
82 "Recursivly compare two nodes."
83 if a.nodeType != b.nodeType or a.nodeName != b.nodeName:
84 return FALSE
85 if a.nodeValue != b.nodeValue:
86 return FALSE
87 aks = a.childNodes
88 bks = b.childNodes
89 if len(aks) != len(bks):
90 return FALSE
91 for (ak, bk) in map(None, aks, bks):
92 if not same(ak, bk):
93 return FALSE
94 return TRUE
96 class InProgress(Exception):
97 "Throw this if the operation will complete later..."
98 class Done(Exception):
99 "Thrown when the chain is completed successfully"
101 class View:
102 def __init__(self, model, callback_handlers = None):
103 """callback_handlers is an (idle_add, idle_remove) tuple"""
104 self.root = None
105 self.displays = []
106 self.lists = []
107 self.single_step = 1 # 0 = Play 1 = Step-into 2 = Step-over
108 self.model = None
109 self.chroots = []
110 self.current_nodes = []
111 self.clipboard = None
112 self.current_attrib = None
114 if not callback_handlers:
115 import gtk
116 self.idle_add, self.idle_remove = gtk.idle_add, gtk.idle_remove
117 else:
118 self.idle_add, self.idle_remove = callback_handlers
120 self.exec_point = None # None, or (Op, Exit)
121 self.rec_point = None # None, or (Op, Exit)
122 self.op_in_progress = None
123 self.idle_cb = 0
124 self.callback_on_return = None # Called when there are no more Ops...
125 self.in_callback = 0 # (not the above callback - this is the playback one)
126 self.innermost_failure = None
127 self.call_on_done = None # Called when there is nowhere to return to
128 self.exec_stack = [] # Ops we are inside (display use only)
130 self.breakpoints = {} # (op, exit) keys, values don't matter
131 self.current_nodes = []
132 self.set_model(model)
134 import debug
135 debug.view = self
137 def get_current(self):
138 if len(self.current_nodes) == 1:
139 return self.current_nodes[0]
140 raise Exception('This operation required exactly one selected node!')
142 def set_model(self, model):
143 if self.root:
144 self.move_to([])
145 self.model.unlock(self.root)
146 self.root = None
147 if self.model:
148 self.model.remove_view(self)
149 self.model.root_program.watchers.remove(self)
150 self.model = model
151 self.model.root_program.watchers.append(self)
152 model.add_view(self)
153 self.set_display_root(self.model.get_root())
154 self.move_to(self.root)
156 def running(self):
157 return self.idle_cb != 0 or self.in_callback
159 def run_new(self, callback = None):
160 "Reset the playback system (stack, step-mode and point)."
161 "Call callback(exit) when execution finishes."
162 if self.idle_cb:
163 self.idle_remove(self.idle_cb)
164 self.idle_cb = 0
165 self.single_step = 0
166 self.innermost_failure = None
167 self.call_on_done = callback
168 self.callback_on_return = None
169 while self.exec_stack:
170 self.pop_stack()
171 self.status_changed()
173 def push_stack(self, op):
174 if not isinstance(op, Op):
175 raise Exception('push_stack: not an Op', op)
176 self.exec_stack.append(op)
177 for l in self.lists:
178 l.update_stack(op)
180 def pop_stack(self):
181 op = self.exec_stack.pop()
182 for l in self.lists:
183 l.update_stack(op)
185 def set_exec(self, pos):
186 if self.op_in_progress:
187 raise Exception("Operation in progress...")
188 if pos and not isinstance(pos[0], Op):
189 raise Exception("Not an (operation, exit) tuple: " + `pos`)
190 self.exec_point = pos
191 #if pos:
192 #print "set_exec: %s:%s" % pos
193 for l in self.lists:
194 l.update_points()
196 def set_rec(self, pos):
197 self.rec_point = pos
198 for l in self.lists:
199 l.update_points()
200 self.status_changed()
202 def record_at_point(self):
203 if not self.exec_point:
204 support.report_error("No current point!")
205 return
206 self.set_rec(self.exec_point)
207 self.set_exec(None)
209 def stop_recording(self):
210 if self.rec_point:
211 self.set_exec(self.rec_point)
212 self.set_rec(None)
213 else:
214 support.report_error("Not recording!")
216 def may_record(self, action):
217 "Perform and, possibly, record this action"
218 rec = self.rec_point
220 if rec:
221 print "RECORD:", rec, action
222 (op, old_exit) = rec
223 new_op = Op(action)
224 op.link_to(new_op, old_exit)
225 self.set_exec(rec)
226 try:
227 self.do_one_step()
228 except InProgress:
229 self.set_rec((new_op, 'next'))
230 return
231 play_op, exit = self.exec_point
232 # (do_one_step may have stopped recording)
233 if self.rec_point:
234 self.set_rec((new_op, exit))
235 self.set_exec(None)
236 return
238 exit = 'next'
239 try:
240 self.do_action(action)
241 except InProgress:
242 pass
243 except Beep:
244 import gtk
245 gtk.gdk_beep()
246 (type, val, tb) = sys.exc_info()
247 #if not val.may_record:
248 # return 0
249 exit = 'fail'
250 except:
251 support.report_exception()
252 raise
254 def add_display(self, display):
255 "Calls move_from(old_node) when we move and update_all() on updates."
256 self.displays.append(display)
257 #print "Added:", self.displays
259 def remove_display(self, display):
260 self.displays.remove(display)
261 #print "Removed, now:", self.displays
262 if not self.displays:
263 self.delete()
265 def update_replace(self, old, new):
266 if old == self.root:
267 self.root = new
268 if old in self.current_nodes:
269 self.model.lock(new)
270 self.model.unlock(old)
271 self.current_nodes.remove(old)
272 self.current_nodes.append(new)
273 self.update_all(new.parentNode)
274 else:
275 self.update_all(new.parentNode)
277 def has_ancestor(self, node, ancestor):
278 while node != ancestor:
279 node = node.parentNode
280 if not node:
281 return FALSE
282 return TRUE
284 def update_all(self, node):
286 # Is the root node still around?
287 if not self.has_ancestor(self.root, self.model.get_root()):
288 # No - reset everything
289 print "[ lost root - using doc root ]"
290 self.root = self.model.doc.documentElement
291 self.chroots = []
292 raise Exception('Root locking error!')
294 # Is the current node still around?
295 for n in self.current_nodes[:]:
296 if not self.has_ancestor(n, self.root):
297 # No - locking error!
298 self.current_nodes.remove(n)
299 raise Exception('Internal locking error on %s!' % n)
301 if not (self.has_ancestor(node, self.root) or self.has_ancestor(self.root, node)):
302 #print "[ change to %s doesn't affect us (root %s) ]" % (node, self.root)
303 return
306 for display in self.displays:
307 display.update_all(node)
309 def delete(self):
310 #print "View deleted"
311 self.model.root_program.watchers.remove(self)
312 self.move_to([])
313 for l in self.lists:
314 l.destroy()
315 self.model.unlock(self.root)
316 self.root = None
317 self.model.remove_view(self)
318 self.model = None
320 # 'nodes' may be either a node or a list of nodes.
321 # If it's a single node, then an 'attrib' node may also be specified
322 def move_to(self, nodes, attrib = None):
323 if self.current_nodes == nodes:
324 return
326 if attrib and attrib.nodeType != Node.ATTRIBUTE_NODE:
327 raise Exception('attrib not of type ATTRIBUTE_NODE!')
329 if type(nodes) != types.ListType:
330 nodes = [nodes]
332 old_nodes = self.current_nodes
333 self.current_nodes = nodes
335 for node in self.current_nodes:
336 self.model.lock(node)
337 for node in old_nodes:
338 self.model.unlock(node)
340 self.current_attrib = attrib
342 for display in self.displays:
343 display.move_from(old_nodes)
345 def move_prev_sib(self):
346 if self.get_current() == self.root or not self.get_current().previousSibling:
347 raise Beep
348 self.move_to(self.get_current().previousSibling)
350 def move_next_sib(self):
351 if self.get_current() == self.root or not self.get_current().nextSibling:
352 raise Beep
353 self.move_to(self.get_current().nextSibling)
355 def move_left(self):
356 new = []
357 for n in self.current_nodes:
358 if n == self.root:
359 raise Beep
360 p = n.parentNode
361 if p not in new:
362 new.append(p)
363 self.move_to(new)
365 def move_right(self):
366 new = []
367 for n in self.current_nodes:
368 kids = n.childNodes
369 if kids:
370 new.append(kids[0])
371 else:
372 raise Beep
373 self.move_to(new)
375 def move_home(self):
376 self.move_to(self.root)
378 def move_end(self):
379 if not self.get_current().childNodes:
380 raise Beep
381 node = self.get_current().childNodes[0]
382 while node.nextSibling:
383 node = node.nextSibling
384 self.move_to(node)
386 def set_display_root(self, root):
387 self.model.lock(root)
388 if self.root:
389 self.model.unlock(self.root)
390 self.root = root
391 self.update_all(root)
393 def enter(self):
394 """Change the display root to a COPY of the selected node.
395 Call Leave to check changes back in."""
396 node = self.get_current()
397 if node is self.root:
398 raise Beep # Locking problems if this happens...
399 if self.model.doc is not node.ownerDocument:
400 raise Exception('Current node not in view!')
401 self.chroots.append((self.model, node))
402 self.set_model(self.model.lock_and_copy(node))
404 def leave(self):
405 """Undo the effect of the last chroot()."""
406 if not self.chroots:
407 raise Beep
409 (old_model, old_node) = self.chroots.pop()
411 copy = old_model.doc.importNode(self.model.get_root(), 1)
412 self.set_model(old_model)
413 old_model.unlock(old_node)
414 old_model.replace_node(old_node, copy)
415 self.move_to([copy])
417 def do_action(self, action):
418 "'action' is a tuple (function, arg1, arg2, ...)"
419 "Performs the action. Returns if action completes, or raises "
420 "InProgress if not (will call resume() later)."
421 if action[0] in record_again:
422 self.last_action = action
423 elif action[0] == 'again':
424 action = self.last_action
425 fn = getattr(self, action[0])
426 exit = 'next'
427 #print "DO:", action[0]
428 self.model.mark()
429 try:
430 new = apply(fn, action[1:])
431 except InProgress:
432 raise
433 except Beep:
434 if not self.op_in_progress:
435 raise
436 exit = 'fail'
437 new = None
438 except:
439 if not self.op_in_progress:
440 raise
441 traceback.print_exc()
442 exit = 'fail'
443 new = None
445 if self.op_in_progress:
446 op = self.op_in_progress
447 self.set_oip(None)
448 self.set_exec((op, exit))
449 if new:
450 self.move_to(new)
452 def breakpoint(self):
453 if self.breakpoints.has_key(self.exec_point):
454 return 1
455 op = self.exec_point[0]
456 if op.program.start == op and op.next == None:
457 return 1 # Empty program
458 return 0
460 def do_one_step(self):
461 "Execute the next op after exec_point, then:"
462 "- position the point on one of the exits return."
463 "- if there is no op to perform, call callback_on_return() or raise Done."
464 "- if the operation is started but not complete, raise InProgress and "
465 " arrange to resume() later."
466 if self.op_in_progress:
467 support.report_error("Already executing something.")
468 raise Done()
469 if not self.exec_point:
470 support.report_error("No current playback point.")
471 raise Done()
472 (op, exit) = self.exec_point
474 if self.single_step == 0 and self.breakpoint():
475 print "Hit a breakpoint! At " + time.ctime(time.time())
476 if self.rec_point:
477 self.set_rec(None)
478 self.single_step = 1
479 for l in self.lists:
480 l.show_prog(op.program)
481 return
483 next = getattr(op, exit)
484 if next:
485 self.set_oip(next)
486 self.do_action(next.action) # May raise InProgress
487 return
489 if exit == 'fail' and not self.innermost_failure:
490 #print "Setting innermost_failure on", op
491 self.innermost_failure = op
493 if self.callback_on_return:
494 cb = self.callback_on_return
495 self.callback_on_return = None
496 cb()
497 else:
498 raise Done()
500 def set_oip(self, op):
501 if op:
502 self.set_exec(None)
503 self.op_in_progress = op
504 for l in self.lists:
505 l.update_points()
507 def fast_global(self, name):
508 "Search for nodes with this name anywhere under the root (//name)"
509 #print "Fast global", name
510 if ':' in name:
511 (prefix, localName) = string.split(name, ':', 1)
512 else:
513 (prefix, localName) = (None, name)
514 if self.current_nodes:
515 src = self.current_nodes[-1]
516 else:
517 src = self.root
518 namespaceURI = self.model.prefix_to_namespace(src, prefix)
519 select = []
520 def add(node):
521 if node.nodeType != Node.ELEMENT_NODE:
522 return
523 if node.localName == localName and node.namespaceURI == namespaceURI:
524 select.append(node)
525 map(add, node.childNodes)
526 add(self.root)
527 self.move_to(select)
529 # Actions...
531 def do_global(self, pattern):
532 if len(self.current_nodes) != 1:
533 self.move_to(self.root)
534 if pattern[:2] == '//':
535 if fast_global.match(pattern):
536 self.fast_global(pattern[2:])
537 return
538 ns = {}
539 if not ns:
540 ns = ext.GetAllNs(self.current_nodes[0])
541 ns['ext'] = FT_EXT_NAMESPACE
542 ns['_'] = ns[None]
543 #print "ns is", ns
544 c = Context.Context(self.get_current(), processorNss = ns)
545 from Ft.Xml.XPath import XPathParser
546 code = XPathParser.new().parse(self.macro_pattern(pattern))
547 #print code
548 nodes = code.evaluate(c)
549 #nodes = XPath.Evaluate(self.macro_pattern(pattern), contextNode = self.get_current())
550 #print "Found", nodes
551 self.move_to(nodes)
553 def select_region(self, path, ns = None):
554 if len(self.current_nodes) == 0:
555 raise Beep
556 src = self.current_nodes[-1]
557 if not ns:
558 ns = ext.GetAllNs(src)
559 ns['ext'] = FT_EXT_NAMESPACE
560 c = Context.Context(src, [src], processorNss = ns)
561 rt = XPath.Evaluate(path, context = c)
562 node = None
563 for x in rt:
564 if not self.has_ancestor(x, self.root):
565 print "[ skipping search result above root ]"
566 continue
567 if not node:
568 node = x
569 if not node:
570 print "*** Search for '%s' in select_region failed" % path
571 print " (namespaces were '%s')" % ns
572 raise Beep
573 if node.parentNode != src.parentNode:
574 print "Nodes must have same parent!"
575 raise Beep
576 on = 0
577 selected = []
578 for n in src.parentNode.childNodes:
579 was_on = on
580 if n is src or n is node:
581 on = not was_on
582 if on or was_on:
583 selected.append(n)
584 self.move_to(selected)
586 def macro_pattern(self, pattern):
587 """Do the @CURRENT@ substitution for an XPath"""
588 if len(self.current_nodes) != 1:
589 return pattern
590 node = self.get_current()
591 if node.nodeType == Node.TEXT_NODE:
592 current = node.data
593 else:
594 current = node.nodeName
595 pattern = pattern.replace('@CURRENT@', current)
596 #print "Searching for", pattern
597 return pattern
599 def do_search(self, pattern, ns = None, toggle = FALSE):
600 if len(self.current_nodes) == 0:
601 src = self.root
602 else:
603 src = self.current_nodes[-1]
604 if not ns:
605 ns = ext.GetAllNs(src)
606 ns['ext'] = FT_EXT_NAMESPACE
607 c = Context.Context(src, [src], processorNss = ns)
609 rt = XPath.Evaluate(self.macro_pattern(pattern), context = c)
610 node = None
611 for x in rt:
612 if not self.has_ancestor(x, self.root):
613 print "[ skipping search result above root ]"
614 continue
615 if not node:
616 node = x
617 #if self.node_to_line[x] > self.current_line:
618 #node = x
619 #break
620 if not node:
621 #print "*** Search for '%s' failed" % pattern
622 #print " (namespaces were '%s')" % ns
623 raise Beep
624 if toggle:
625 new = self.current_nodes[:]
626 if node in new:
627 new.remove(node)
628 else:
629 new.append(node)
630 self.move_to(new)
631 else:
632 self.move_to(node)
634 def do_text_search(self, pattern):
635 pattern = self.macro_pattern(pattern)
636 return self.do_search("//text()[ext:match('%s')]" % pattern)
638 def subst(self, replace, with):
639 "re search and replace on the current node"
640 nodes = self.current_nodes[:]
641 check = len(nodes) == 1
642 a = self.current_attrib
643 if a:
644 new = re.sub(replace, with, a.value)
645 a = self.model.set_attrib(nodes[0], a.name, new)
646 self.move_to(nodes[0], a)
647 else:
648 self.move_to([])
649 final = []
650 for n in nodes:
651 if n.nodeType == Node.TEXT_NODE:
652 old = str(n.data).replace('\n', ' ')
653 new, num = re.subn(replace, with, old)
654 if check and not num:
655 self.move_to(n)
656 raise Beep
657 self.model.set_data(n, new)
658 final.append(n)
659 elif n.nodeType == Node.ELEMENT_NODE:
660 old = str(n.nodeName)
661 new, num = re.subn(replace, with, old)
662 if check and not num:
663 self.move_to(n)
664 raise Beep
665 new_ns, x = self.model.split_qname(n, new)
666 final.append(self.model.set_name(n, new_ns, new))
667 else:
668 self.move_to(n)
669 raise Beep
670 self.move_to(final)
672 def python(self, expr):
673 "Replace node with result of expr(old_value)"
674 if self.get_current().nodeType == Node.TEXT_NODE:
675 vars = {'x': self.get_current().data, 're': re, 'sub': re.sub, 'string': string}
676 result = eval(expr, vars)
677 new = self.python_to_node(result)
678 node = self.get_current()
679 self.move_to([])
680 self.model.replace_node(node, new)
681 self.move_to(new)
682 else:
683 raise Beep
685 def resume(self, exit = 'next'):
686 "After raising InProgress, call this to start moving again."
687 if self.op_in_progress:
688 op = self.op_in_progress
689 self.set_oip(None)
690 self.set_exec((op, exit))
691 if not self.single_step:
692 self.sched()
693 self.status_changed()
694 else:
695 print "(nothing to resume)"
697 def ask(self, q):
698 def ask_cb(result, self = self):
699 if result is None:
700 exit = 'fail'
701 else:
702 self.clipboard = self.model.doc.createTextNode(result)
703 exit = 'next'
704 self.resume(exit)
705 from GetArg import GetArg
706 box = GetArg('Input:', ask_cb, [q], destroy_return = 1)
707 raise InProgress
709 def python_to_node(self, data):
710 "Convert a python data structure into a tree and return the root."
711 if type(data) == types.ListType:
712 list = self.model.doc.createElementNS(DOME_NS, 'dome:list')
713 for x in data:
714 list.appendChild(self.python_to_node(x))
715 return list
716 return self.model.doc.createTextNode(str(data))
718 def yank(self, deep = 1):
719 if self.current_attrib:
720 a = self.current_attrib
722 self.clipboard = self.model.doc.createElementNS(a.namespaceURI, a.nodeName)
723 self.clipboard.appendChild(self.model.doc.createTextNode(a.value))
724 else:
725 self.clipboard = self.model.doc.createDocumentFragment()
726 for n in self.current_nodes:
727 c = n.cloneNode(deep)
728 #print n, "->", c
729 self.clipboard.appendChild(c)
731 #print "Clip now", self.clipboard
733 def shallow_yank(self):
734 self.yank(0)
736 def delete_shallow(self):
737 nodes = self.current_nodes[:]
738 if not nodes:
739 return
740 if self.root in nodes:
741 raise Beep
742 self.shallow_yank()
743 self.move_to([])
744 for n in nodes:
745 self.model.delete_shallow(n)
746 self.move_home()
748 def delete_node_no_clipboard(self):
749 self.delete_node(yank = 0)
751 def delete_node(self, yank = 1):
752 nodes = self.current_nodes[:]
753 if not nodes:
754 return
755 if yank:
756 self.yank()
757 if self.current_attrib:
758 ca = self.current_attrib
759 self.current_attrib = None
760 self.model.set_attrib(self.get_current(), ca.name, None)
761 return
762 if self.root in nodes:
763 raise Beep
764 self.move_to([]) # Makes things go *much* faster!
765 new = []
766 for x in nodes:
767 p = x.parentNode
768 #print "Delete %s, parent %s" % (x, p)
769 if p not in new:
770 new.append(p)
771 self.move_to(new)
772 self.model.delete_nodes(nodes)
774 def undo(self):
775 nodes = self.current_nodes[:]
776 self.move_to([])
777 self.model.unlock(self.root)
778 try:
779 self.model.undo()
780 finally:
781 self.model.lock(self.root)
782 self.move_to(filter(lambda x: self.has_ancestor(x, self.root), nodes))
784 def redo(self):
785 nodes = self.current_nodes[:]
786 self.move_to([])
787 self.model.unlock(self.root)
788 try:
789 self.model.redo()
790 finally:
791 self.model.lock(self.root)
792 self.move_to(filter(lambda x: self.has_ancestor(x, self.root), nodes))
794 def default_done(self, exit):
795 "Called when execution of a program returns. op_in_progress has been "
796 "restored - move to the exit."
797 #print "default_done(%s)" % exit
798 if self.op_in_progress:
799 op = self.op_in_progress
800 self.set_oip(None)
801 self.set_exec((op, exit))
802 else:
803 print "No operation to return to!"
804 c = self.call_on_done
805 if c:
806 self.call_on_done = None
807 c(exit)
808 elif exit == 'fail':
809 self.jump_to_innermost_failure()
810 raise Done()
812 def jump_to_innermost_failure(self):
813 assert self.innermost_failure != None
815 print "Returning to innermost failure:", self.innermost_failure
816 self.set_exec((self.innermost_failure, 'fail'))
817 for l in self.lists:
818 if hasattr(l, 'set_innermost_failure'):
819 l.set_innermost_failure(self.innermost_failure)
821 def play(self, name, done = None):
822 "Play this macro. When it returns, restore the current op_in_progress (if any)"
823 "and call done(exit). Default for done() moves exec_point."
824 "done() is called from do_one_step() - usual rules apply."
826 prog = self.name_to_prog(name)
827 self.innermost_failure = None
829 if not done:
830 done = self.default_done
832 def cbor(self = self, op = self.op_in_progress, done = done,
833 name = name,
834 old_cbor = self.callback_on_return,
835 old_ss = self.single_step):
836 "We're in do_one_step..."
838 #print "Return from '%s'..." % name
840 if old_ss == 2 and self.single_step == 0:
841 self.single_step = old_ss
842 self.callback_on_return = old_cbor
844 o, exit = self.exec_point
845 if op:
846 #print "Resume op '%s' (%s)" % (op.program.name, op)
847 self.pop_stack()
848 self.set_oip(op)
849 return done(exit)
851 self.callback_on_return = cbor
853 if self.single_step == 2:
854 self.single_step = 0
856 if self.op_in_progress:
857 self.push_stack(self.op_in_progress)
858 self.set_oip(None)
859 self.set_exec((prog.start, 'next'))
860 self.sched()
861 self.status_changed()
862 raise InProgress
864 def sched(self):
865 if self.op_in_progress:
866 raise Exception("Operation in progress")
867 if self.idle_cb:
868 raise Exception("Already playing!")
869 self.idle_cb = self.idle_add(self.play_callback)
871 def play_callback(self):
872 self.idle_remove(self.idle_cb)
873 self.idle_cb = 0
874 try:
875 self.in_callback = 1
876 try:
877 self.do_one_step()
878 finally:
879 self.in_callback = 0
880 except Done:
881 (op, exit) = self.exec_point
882 if exit == 'fail' and self.innermost_failure:
883 self.jump_to_innermost_failure()
884 print "Done, at " + time.ctime(time.time())
885 self.run_new()
886 return 0
887 except InProgress:
888 #print "InProgress"
889 return 0
890 except:
891 type, val, tb = sys.exc_info()
892 list = traceback.extract_tb(tb)
893 stack = traceback.format_list(list[-2:])
894 ex = traceback.format_exception_only(type, val) + ['\n\n'] + stack
895 traceback.print_exception(type, val, tb)
896 print "Error in do_one_step(): stopping playback"
897 node = self.op_in_progress
898 self.set_oip(None)
899 self.set_exec((node, 'fail'))
900 self.status_changed()
901 return 0
902 if self.op_in_progress or self.single_step:
903 self.status_changed()
904 return 0
905 self.sched()
906 return 0
908 def status_changed(self):
909 for display in self.displays:
910 if hasattr(display, 'update_state'):
911 display.update_state()
913 def map(self, name):
914 print "Map", name
916 nodes = self.current_nodes[:]
917 if not nodes:
918 print "map of nothing: skipping..."
919 return
920 inp = [nodes, None] # Nodes, next
921 def next(exit = exit, self = self, name = name, inp = inp):
922 "This is called while in do_one_step() - normal rules apply."
923 nodes, next = inp
924 print "[ %d to go ]" % len(nodes),
925 if exit == 'fail':
926 print "Map: nodes remaining, but an error occurred..."
927 return self.default_done(exit)
928 while nodes and nodes[0].parentNode == None:
929 print "Skipping deleted node", nodes[0]
930 del nodes[0]
931 if not nodes:
932 return self.default_done(exit)
933 self.move_to(nodes[0])
934 del nodes[0]
935 if not nodes:
936 next = None
937 #print "Map: calling play (%d after this)" % len(nodes)
938 self.play(name, done = next) # Should raise InProgress
939 if nodes is self.current_nodes:
940 raise Exception("Slice failed!")
941 inp[1] = next
942 next('next')
944 def name_to_prog(self, name):
945 comps = string.split(name, '/')
946 prog = self.model.root_program
947 if prog.name != comps[0]:
948 raise Exception("No such program as '%s'!" % name)
949 del comps[0]
950 while comps:
951 prog = prog.subprograms[comps[0]]
952 del comps[0]
953 return prog
955 def change_node(self, new_data):
956 nodes = self.current_nodes
957 if not nodes:
958 return
959 self.move_to([])
960 if nodes[0].nodeType == Node.ELEMENT_NODE:
961 # Slow, so do this here, even if vaguely incorrect...
962 if ':' in new_data:
963 (prefix, localName) = string.split(new_data, ':', 1)
964 else:
965 (prefix, localName) = (None, new_data)
966 namespaceURI = self.model.prefix_to_namespace(nodes[0], prefix)
967 out = []
968 for node in nodes:
969 if node is self.root:
970 self.model.unlock(self.root)
971 new = self.model.set_name(node, namespaceURI, new_data)
972 self.model.lock(new)
973 self.root = new
974 else:
975 new = self.model.set_name(node, namespaceURI, new_data)
976 out.append(new)
977 self.move_to(out)
978 else:
979 for node in nodes:
980 self.model.set_data(node, new_data)
981 self.move_to(nodes)
983 def add_node(self, where, data):
984 cur = self.get_current()
985 if where[1] == 'e':
986 if ':' in data:
987 (prefix, localName) = string.split(data, ':', 1)
988 else:
989 (prefix, localName) = (None, data)
990 namespaceURI = self.model.prefix_to_namespace(self.get_current(), prefix)
991 new = self.model.doc.createElementNS(namespaceURI, data)
992 else:
993 new = self.model.doc.createTextNode(data)
995 try:
996 if where[0] == 'i':
997 self.model.insert_before(cur, new)
998 elif where[0] == 'a':
999 self.model.insert_after(cur, new)
1000 else:
1001 self.model.insert(cur, new)
1002 except:
1003 raise Beep
1005 self.move_to(new)
1007 def http_post(self):
1008 node = self.get_current()
1009 attrs = node.attributes
1010 post = []
1011 for (ns,name) in attrs.keys():
1012 if ns is None:
1013 post.append((str(name),
1014 str(attrs[(ns, name)].value)))
1015 node = self.suck_node(node, post_data = urllib.urlencode(post))
1016 if node:
1017 self.move_to(node)
1019 def suck(self):
1020 nodes = self.current_nodes[:]
1021 self.move_to([])
1022 final = []
1023 for x in nodes:
1024 try:
1025 new = self.suck_node(x)
1026 finally:
1027 self.move_to(x)
1028 final.append(new)
1029 self.move_to(final)
1031 def suck_node(self, node, post_data = None):
1032 uri = None
1033 if node.nodeType == Node.TEXT_NODE:
1034 uri = node.nodeValue
1035 else:
1036 if self.current_attrib:
1037 uri = self.current_attrib.value
1038 elif node.hasAttributeNS(None, 'uri'):
1039 uri = node.getAttributeNS(None, 'uri')
1040 else:
1041 for attr in node.attributes.keys():
1042 uri = node.attributes[attr].value
1043 if uri.find('//') != -1 or uri.find('.htm') != -1:
1044 break
1045 if not uri:
1046 print "Can't suck", node
1047 raise Beep
1048 if uri.find('//') == -1:
1049 base = self.model.get_base_uri(node)
1050 #print "Relative URI..."
1051 if base:
1052 #print "Base URI is:", base, "add", uri
1053 uri = urlparse.urljoin(base, uri)
1054 else:
1055 pass
1056 #print "Warning: Can't find 'uri' attribute!"
1058 print "Sucking", uri
1060 if post_data is not None:
1061 print "POSTING", post_data
1062 stream = urllib.urlopen(uri, post_data)
1063 headers = stream.info().headers
1064 last_mod = None
1065 for x in headers:
1066 if x.lower().startswith('last-modified:'):
1067 last_mod = x[14:].strip()
1068 break
1070 current_last_mod = node.getAttributeNS(None, 'last-modified')
1071 if current_last_mod and last_mod:
1072 if current_last_mod == last_mod:
1073 self.model.set_attrib(node, 'modified', None)
1074 print "not modified => not sucking!\n"
1075 return
1077 print "Fetching page contents..."
1078 data = stream.read()
1079 print "got data... tidying..."
1081 (r, w) = os.pipe()
1082 child = os.fork()
1083 if child == 0:
1084 # We are the child
1085 try:
1086 os.close(r)
1087 os.dup2(w, 1)
1088 os.close(w)
1089 tin = os.popen('tidy -asxml 2>/dev/null', 'w')
1090 tin.write(data)
1091 tin.close()
1092 finally:
1093 os._exit(0)
1094 os.close(w)
1096 data = os.fdopen(r).read()
1097 os.waitpid(child, 0)
1099 old_md5 = node.getAttributeNS(None, 'md5_sum')
1101 import md5
1102 new_md5 = md5.new(data).hexdigest()
1104 if old_md5 and new_md5 == old_md5:
1105 self.model.set_attrib(node, 'modified', None)
1106 print "MD5 sums match => not parsing!"
1107 return
1109 reader = PyExpat.Reader()
1110 print "parsing...",
1112 from Ft.Xml.InputSource import InputSourceFactory
1113 from Ft.Xml.cDomlette import nonvalParse
1114 isrc = InputSourceFactory()
1116 try:
1117 root = nonvalParse(isrc.fromString(data, uri))
1118 ext.StripHtml(root)
1119 except:
1120 print "parsing failed!"
1121 print "Data was:"
1122 print data
1123 #support.report_exception()
1124 raise Beep
1125 else:
1126 print "parse OK...",
1128 new = node.ownerDocument.importNode(root.documentElement, 1)
1129 new.setAttributeNS(None, 'uri', uri)
1131 if last_mod:
1132 new.setAttributeNS(None, 'last-modified', last_mod)
1133 new.setAttributeNS(None, 'modified', 'yes')
1134 new.setAttributeNS(None, 'md5_sum', new_md5)
1136 self.move_to([])
1137 if node == self.root:
1138 self.model.unlock(self.root)
1139 self.model.replace_node(self.root, new)
1140 #self.model.strip_space(new) (not sure we need this)
1141 self.model.lock(new)
1142 self.root = new
1143 else:
1144 self.model.replace_node(node, new)
1145 #self.model.strip_space(new)
1147 print "Loaded."
1148 return new
1150 def dom_from_command(self, command, callback = None, old_md5 = None):
1151 """Execute shell command 'command' in the background.
1152 Parse the output as XML. When done, call callback(doc_root, md5).
1153 If old_md5 is given, compare the MD5 of the document with it,
1154 and do callback(None, "Same") if they match.
1156 #print command
1157 cout = os.popen(command)
1159 all = ["", None]
1160 def got_all(data, cb = callback, m5 = old_md5):
1161 import md5
1162 new_md5 = md5.new(data).hexdigest()
1164 if m5 and new_md5 == m5:
1165 cb(None, "Same")
1166 return
1168 reader = PyExpat.Reader()
1169 print "Parsing..."
1171 try:
1172 root = reader.fromStream(StringIO(data))
1173 ext.StripHtml(root)
1174 except:
1175 print "dom_from_command: parsing failed"
1176 support.report_exception()
1177 root = None
1178 cb(root, new_md5)
1180 # XXX: only for nogui...
1181 got_all(cout.read())
1182 return
1184 def got_html(src, cond, all = all, got_all = got_all):
1185 data = src.read(100)
1186 if data:
1187 all[0] += data
1188 return
1189 input_remove(all[1])
1190 src.close()
1191 got_all(all[0])
1193 all[1] = input_add(cout, GDK.INPUT_READ, got_html)
1195 def put_before(self):
1196 node = self.get_current()
1197 if self.clipboard == None:
1198 raise Beep
1199 new = self.clipboard.cloneNode(1)
1200 try:
1201 self.model.insert_before(node, new)
1202 except:
1203 raise Beep
1205 def put_after(self):
1206 node = self.get_current()
1207 if self.clipboard == None:
1208 raise Beep
1209 new = self.clipboard.cloneNode(1)
1210 self.model.insert_after(node, new)
1212 def put_replace(self):
1213 node = self.get_current()
1214 if self.clipboard == None:
1215 raise Beep
1216 if self.current_attrib:
1217 if self.clipboard.nodeType == Node.DOCUMENT_FRAGMENT_NODE:
1218 value = self.clipboard.childNodes[0].data
1219 else:
1220 value = self.clipboard.data
1221 a = self.current_attrib
1222 value = value.replace('\n', ' ')
1223 self.model.set_attrib(node, a.name, value)
1224 return
1225 if self.clipboard.nodeType == Node.DOCUMENT_FRAGMENT_NODE:
1226 if len(self.clipboard.childNodes) != 1:
1227 raise Beep
1228 new = self.clipboard.childNodes[0].cloneNode(1)
1229 else:
1230 new = self.clipboard.cloneNode(1)
1231 if new.nodeType != Node.ELEMENT_NODE:
1232 raise Beep
1233 self.move_to([])
1234 try:
1235 if node == self.root:
1236 self.model.unlock(self.root)
1237 try:
1238 self.model.replace_node(self.root, new)
1239 self.root = new
1240 finally:
1241 self.model.lock(self.root)
1242 else:
1243 self.model.replace_node(node, new)
1244 self.move_to(new)
1245 except:
1246 raise Beep
1248 def put_as_child(self):
1249 node = self.get_current()
1250 if self.clipboard == None:
1251 raise Beep
1252 new = self.clipboard.cloneNode(1)
1253 if new.nodeType == Node.DOCUMENT_FRAGMENT_NODE:
1254 to = []
1255 for n in new.childNodes:
1256 to.append(n)
1257 else:
1258 to = new
1259 try:
1260 self.model.insert(node, new, index = 0)
1261 except:
1262 raise Beep
1264 self.move_to(to)
1266 def yank_value(self):
1267 if not self.current_attrib:
1268 raise Beep
1269 value = self.current_attrib.value
1270 self.clipboard = self.model.doc.createTextNode(value)
1271 #print "Clip now", self.clipboard
1273 def yank_attribs(self, name):
1274 self.clipboard = self.model.doc.createDocumentFragment()
1275 if name:
1276 if not self.get_current().hasAttribute(name):
1277 raise Beep
1278 attribs = [self.get_current().getAttributeNode(name)]
1279 else:
1280 attribs = []
1281 dict = self.get_current().attributes
1282 for a in dict.keys():
1283 attribs.append(dict[a])
1285 # Make sure the attributes always come out in the same order
1286 # (helps with macros).
1287 def by_name(a, b):
1288 diff = cmp(a.name, b.name)
1289 if diff == 0:
1290 diff = cmp(a.namespaceURI, b.namespaceURI)
1291 return diff
1293 attribs.sort(by_name)
1294 for a in attribs:
1295 n = self.model.doc.createElementNS(a.namespaceURI, a.nodeName)
1296 n.appendChild(self.model.doc.createTextNode(a.value))
1297 self.clipboard.appendChild(n)
1298 #print "Clip now", self.clipboard
1300 def paste_attribs(self):
1301 if self.clipboard.nodeType == Node.DOCUMENT_FRAGMENT_NODE:
1302 attribs = self.clipboard.childNodes
1303 else:
1304 attribs = [self.clipboard]
1305 new = []
1306 for a in attribs:
1307 try:
1308 new.append((a.nodeName, a.childNodes[0].data))
1309 except:
1310 raise Beep
1311 for node in self.current_nodes:
1312 # XXX: Set NS attribs first...
1313 for (name, value) in new:
1314 self.model.set_attrib(node, name, value)
1316 def compare(self):
1317 "Ensure that all selected nodes have the same value."
1318 if len(self.current_nodes) < 2:
1319 raise Beep # Not enough nodes!
1320 base = self.current_nodes[0]
1321 for n in self.current_nodes[1:]:
1322 if not same(base, n):
1323 raise Beep(may_record = 1)
1325 def fail(self):
1326 raise Beep(may_record = 1)
1328 def attribute(self, namespace = None, attrib = ''):
1329 node = self.get_current()
1331 if attrib == '':
1332 self.move_to(node)
1333 return
1335 if attrib == 'xmlns':
1336 attrib = None
1337 #print "(ns, attrib)", `namespace`, attrib
1339 a = node.attributes.get((namespace, attrib), None)
1341 if a:
1342 self.move_to(node, a)
1343 else:
1344 print "No such attribute"
1345 raise Beep()
1347 def set_attrib(self, value):
1348 a = self.current_attrib
1349 if not a:
1350 raise Beep()
1351 node = self.get_current()
1352 a = self.model.set_attrib(node, a.name, value)
1353 self.move_to(node, a)
1355 def add_attrib(self, UNUSED, name, value = ''):
1356 if name.startswith('xmlns:'):
1357 print "*** SET NS ATTRIB ***", self.op_in_progress.program
1359 node = self.get_current()
1360 a = self.model.set_attrib(node, name, value)
1361 self.move_to(node, a)
1363 def load_html(self, path):
1364 "Replace root with contents of this HTML file."
1365 print "Reading HTML..."
1366 command = "tidy -asxml '%s' 2>/dev/null" % path
1368 def done(root, md5, self = self):
1369 print "Loaded!"
1370 new = self.root.ownerDocument.importNode(root.documentElement, 1)
1372 if self.root:
1373 self.model.unlock(self.root)
1374 self.move_to([])
1375 self.model.replace_node(self.root, new)
1376 self.model.lock(new)
1377 self.root = new
1378 self.move_to(self.root)
1380 self.dom_from_command(command, done)
1382 def load_xml(self, path):
1383 "Replace root with contents of this XML (or Dome) file."
1384 reader = PyExpat.Reader()
1385 new_doc = reader.fromUri(path)
1386 self.load_node(new_doc.documentElement)
1388 def load_node(self, root):
1389 new = self.model.doc.importNode(root, 1)
1391 self.model.strip_space(new)
1393 if self.root:
1394 self.model.unlock(self.root)
1395 self.move_to([])
1396 self.model.replace_node(self.root, new)
1397 self.model.lock(new)
1398 self.root = new
1399 self.move_to(self.root)
1401 def select_dups(self):
1402 node = self.get_current()
1403 select = []
1404 for n in node.parentNode.childNodes:
1405 if n is node:
1406 continue
1407 if same(node, n):
1408 select.append(n)
1409 self.move_to(select)
1411 def show_html(self):
1412 from HTML import HTML
1413 HTML(self.model, self.get_current()).show()
1415 def show_canvas(self):
1416 from Canvas import Canvas
1417 Canvas(self, self.get_current()).show()
1419 def toggle_hidden(self):
1420 nodes = self.current_nodes[:]
1421 self.move_to([])
1422 for node in nodes:
1423 if node.hasAttributeNS(None, 'hidden'):
1424 new = None
1425 else:
1426 new = 'yes'
1427 self.model.set_attrib(node, 'hidden', new, with_update = 0)
1428 self.model.update_all(self.root)
1429 self.move_to(nodes)
1431 def soap_send(self):
1432 copy = node_to_xml(self.get_current())
1433 env = copy.documentElement
1435 if env.namespaceURI != 'http://schemas.xmlsoap.org/soap/envelope/':
1436 support.report_error("Not a SOAP-ENV:Envelope (bad namespace)")
1437 raise Done()
1438 if env.localName != 'Envelope':
1439 support.report_error("Not a SOAP-ENV:Envelope (bad local name)")
1440 raise Done()
1442 if len(env.childNodes) != 2:
1443 support.report_error("SOAP-ENV:Envelope must have one header and one body")
1444 raise Done()
1446 kids = elements(env)
1447 head = kids[0]
1448 body = kids[1]
1450 if head.namespaceURI != 'http://schemas.xmlsoap.org/soap/envelope/' or \
1451 head.localName != 'Head':
1452 support.report_error("First child must be a SOAP-ENV:Head element")
1453 raise Done()
1455 if body.namespaceURI != 'http://schemas.xmlsoap.org/soap/envelope/' or \
1456 body.localName != 'Body':
1457 support.report_error("Second child must be a SOAP-ENV:Body element")
1458 raise Done()
1460 sft = None
1461 for header in elements(head):
1462 if header.namespaceURI == DOME_NS and header.localName == 'soap-forward-to':
1463 sft = header
1464 break
1465 print header.namespaceURI
1466 print header.localName
1468 if not sft:
1469 support.report_error("Head must contain a dome:soap-forward-to element")
1470 raise Done()
1472 dest = sft.childNodes[0].data
1473 parent = sft.parentNode
1474 if len(elements(parent)) == 1:
1475 sft = parent
1476 parent = sft.parentNode # Delete the whole header
1477 parent.removeChild(sft)
1479 import httplib, urlparse
1481 (scheme, addr, path, p, q, f) = urlparse.urlparse(dest, allow_fragments = 0)
1482 if scheme != 'http':
1483 support.report_error("SOAP is only supported for 'http:' -- sorry!")
1484 raise Done()
1486 stream = StrGrab()
1487 ext.PrettyPrint(copy, stream = stream)
1488 message = stream.data
1490 conn = httplib.HTTP(addr)
1491 conn.putrequest("POST", path)
1492 conn.putheader('Content-Type', 'text/xml; charset="utf-8"')
1493 conn.putheader('Content-Length', str(len(message)))
1494 conn.putheader('SOAPAction', '')
1495 conn.endheaders()
1496 conn.send(message)
1497 (code, r_mess, r_headers) = conn.getreply()
1499 reply = conn.getfile().read()
1500 print "Got:\n", reply
1502 reader = PyExpat.Reader()
1503 new_doc = reader.fromString(reply)
1504 print new_doc
1506 new = self.model.doc.importNode(new_doc.documentElement, 1)
1508 self.model.strip_space(new)
1510 old = self.get_current()
1511 self.move_to([])
1512 self.model.replace_node(old, new)
1513 self.move_to(new)
1515 def program_changed(self, changed_op):
1516 print "Check points..."
1517 if self.rec_point:
1518 (op, exit) = self.rec_point
1519 if not op.program:
1520 print "Lost rec_point"
1521 self.rec_point = None
1522 if self.exec_point:
1523 (op, exit) = self.exec_point
1524 if not op.program:
1525 print "Lost exec_point"
1526 self.exec_point = None
1527 for l in self.lists:
1528 l.update_points()
1529 self.status_changed()
1531 def prog_tree_changed(self):
1532 pass
1534 def export_all(self):
1535 doc = implementation.createDocument(DOME_NS, 'dome', None)
1536 node = self.model.root_program.to_xml(doc)
1537 doc.documentElement.appendChild(node)
1538 node = doc.createElementNS(DOME_NS, 'dome-data')
1539 doc.documentElement.appendChild(node)
1541 if self.chroots:
1542 print "*** WARNING: Saving from a chroot!"
1543 model = self.model
1544 data = doc.importNode(model.doc.documentElement, 1)
1545 node.appendChild(data)
1547 return doc
1549 def blank_all(self):
1550 doc = implementation.createDocument(None, 'root', None)
1551 self.move_home()
1552 self.clipboard = self.model.doc.createElementNS(None, 'root')
1553 self.put_replace()
1555 class StrGrab:
1556 data = ''
1558 def write(self, str):
1559 self.data += str