Also merge <restricts> elements
[0publish.git] / xmltools.py
blobee823a1d3ebcba6e2323965f5e05c8fa436075c5
1 from xml.dom import Node, XMLNS_NAMESPACE
3 XMLNS_INTERFACE = "http://zero-install.sourceforge.net/2004/injector/interface"
4 XMLNS_COMPILE = "http://zero-install.sourceforge.net/2006/namespaces/0compile"
6 def data(node):
7 """Return all the text directly inside this DOM Node."""
8 return ''.join([text.nodeValue for text in node.childNodes
9 if text.nodeType == Node.TEXT_NODE])
11 def set_data(elem, value):
12 """Replace all children of 'elem' with a single text node containing 'value'"""
13 for node in elem.childNodes:
14 elem.removeChild(node)
15 if value:
16 text = elem.ownerDocument.createTextNode(value)
17 elem.appendChild(text)
19 def indent_of(x):
20 """If x's previous sibling is whitespace, return its length. Otherwise, return 0."""
21 indent = x.previousSibling
22 if indent and indent.nodeType == Node.TEXT_NODE:
23 spaces = indent.nodeValue.split('\n')[-1]
24 if spaces.strip() == '':
25 return len(spaces)
26 return 0
28 def insert_before(new, next):
29 indent_depth = indent_of(next)
30 new_parent = next.parentNode
32 prev = next.previousSibling
33 if prev and prev.nodeType == Node.TEXT_NODE:
34 next = prev
36 new_parent.insertBefore(new, next)
37 if indent_depth:
38 text = new_parent.ownerDocument.createTextNode('\n' + (' ' * indent_depth))
39 new_parent.insertBefore(text, new)
41 def insert_after(new, prev):
42 indent_depth = indent_of(prev)
43 new_parent = prev.parentNode
45 if prev.nextSibling:
46 new_parent.insertBefore(new, prev.nextSibling)
47 else:
48 new_parent.appendChild(new, new_parent)
50 if indent_depth:
51 text = new_parent.ownerDocument.createTextNode('\n' + (' ' * indent_depth))
52 new_parent.insertBefore(text, new)
54 def insert_element(new, parent, before = []):
55 indent = indent_of(parent) + 2 # Default indent
56 last_element = None
57 for x in parent.childNodes:
58 if x.nodeType == Node.ELEMENT_NODE:
59 indent = indent or indent_of(x)
60 if x.localName in before:
61 parent.insertBefore(new, last_element.nextSibling)
62 break
63 last_element = x
64 else:
65 if last_element:
66 parent.insertBefore(new, last_element.nextSibling)
67 else:
68 had_children = bool(list(child_elements(parent)))
69 parent.appendChild(new)
70 if not had_children:
71 final_indent = '\n' + (' ' * indent_of(parent))
72 parent.appendChild(parent.ownerDocument.createTextNode(final_indent))
73 if indent:
74 indent_text = parent.ownerDocument.createTextNode('\n' + (' ' * indent))
75 parent.insertBefore(indent_text, new)
77 def create_element(parent, name, uri = XMLNS_INTERFACE, before = []):
78 """Create a new child element with the given name.
79 Add it as far down the list of children as possible, but before
80 any element in the 'before' set. Indent it sensibly."""
81 new = parent.ownerDocument.createElementNS(uri, name)
82 insert_element(new, parent, before)
83 return new
85 def remove_element(elem):
86 """Remove 'elem' and any whitespace before it."""
87 parent = elem.parentNode
88 prev = elem.previousSibling
89 if prev and prev.nodeType == Node.TEXT_NODE:
90 if prev.nodeValue.strip() == '':
91 parent.removeChild(prev)
92 parent.removeChild(elem)
94 whitespace = []
95 for x in parent.childNodes:
96 if x.nodeType != Node.TEXT_NODE: return
97 if x.nodeValue.strip(): return
98 whitespace.append(x)
100 # Nothing but white-space left
101 for w in whitespace:
102 parent.removeChild(w)
104 def format_para(para):
105 """Turn new-lines into spaces, removing any blank lines."""
106 lines = [l.strip() for l in para.split('\n')]
107 return ' '.join(filter(None, lines))
109 def attrs_match(elem, attrs):
110 for x in attrs:
111 if not elem.hasAttribute(x): return False
112 if elem.getAttribute(x) != attrs[x]: return False
113 return True
115 def child_elements(parent):
116 """Yield all direct child elements."""
117 for x in parent.childNodes:
118 if x.nodeType == Node.ELEMENT_NODE:
119 yield x
121 def children(parent, localName, uri = XMLNS_INTERFACE, attrs = {}):
122 """Yield all direct child elements with this name and attributes."""
123 for x in parent.childNodes:
124 if x.nodeType == Node.ELEMENT_NODE:
125 if x.nodeName == localName and x.namespaceURI == uri and attrs_match(x, attrs):
126 yield x
128 def singleton_text(parent, localName, uri = XMLNS_INTERFACE):
129 """Return the text of the first child element with this name, or None
130 if there aren't any."""
131 elements = list(children(parent, localName, uri))
132 if elements:
133 return data(elements[0])
135 def set_or_remove(element, attr_name, value):
136 if value:
137 element.setAttribute(attr_name, value)
138 elif element.hasAttribute(attr_name):
139 element.removeAttribute(attr_name)
141 namespace_prefixes = {} # Namespace -> prefix
143 def register_namespace(namespace, prefix = None):
144 """Return the prefix to use for a namespace.
145 If none is registered, create a new one based on the suggested prefix.
146 @param namespace: namespace to register / query
147 @param prefix: suggested prefix
148 @return: the actual prefix
150 existing_prefix = namespace_prefixes.get(namespace, None)
151 if existing_prefix:
152 return existing_prefix
154 if not prefix:
155 prefix = 'ns'
157 # Find a variation on 'prefix' that isn't used yet, if necessary
158 orig_prefix = prefix
159 n = 0
160 while prefix in namespace_prefixes.values():
161 print "Prefix %s already in %s, not %s" % (prefix, namespace_prefixes, namespace)
162 n += 1
163 prefix = orig_prefix + str(n)
164 namespace_prefixes[namespace] = prefix
166 return prefix
168 def add_attribute_ns(element, uri, name, value):
169 """Set an attribute, giving it the correct prefix or namespace declarations needed."""
170 if not uri:
171 element.setAttributeNS(None, name, value)
172 else:
173 prefix = register_namespace(uri)
174 element.setAttributeNS(uri, '%s:%s' % (prefix, name), value)
175 element.ownerDocument.documentElement.setAttributeNS(XMLNS_NAMESPACE, 'xmlns:' + prefix, uri)