Switched to expat parser for major speed boost.
[mime-editor.git] / type.py
blob0b9c14a8fe3b72ff732ca3e69e1ae74a14059313
1 import os
2 import rox
3 from rox import g
4 from xml.parsers import expat
5 from xml.dom import XML_NAMESPACE
7 types = {}
9 home_mime = os.path.join(os.environ['HOME'], '.mime')
10 user_override = os.path.join(home_mime, 'packages', 'Override.xml')
12 FREE_NS='http://www.freedesktop.org/standards/shared-mime-info'
14 def get_type(name):
15 if name not in types:
16 types[name] = MIME_Type(name)
17 return types[name]
19 class MIME_Type:
20 def __init__(self, name):
21 assert name not in types
22 self.media, self.subtype = name.split('/')
24 self.comments = []
25 self.globs = []
26 self.magic = []
27 self.xml = []
28 self.others = []
30 def add_from_node(self, file):
31 self.sources.append(Source(file))
33 def add_comment(self, lang, comment):
34 self.comments.append((lang, comment))
36 def add_xml(self, uri, name):
37 self.xml.append((uri, name))
39 def add_magic(self, prio, root):
40 self.magic.append((prio, root))
42 def add_other(self, element):
43 self.others.append(element)
45 def add_glob(self, pattern):
46 self.globs.append(pattern)
48 def get_comment(self):
49 best = None
50 for lang, comment in self.comments:
51 if not lang:
52 return comment
53 best = comment
54 return best or self.get_name()
56 def get_name(self):
57 return self.media + '/' + self.subtype
59 def get_comments(self): return map(Comment, self.comments)
60 def get_globs(self): return map(Glob, self.globs)
61 def get_magic(self): return map(Magic, self.magic)
62 def get_xml(self): return map(XML, self.xml)
63 def get_others(self): return map(Others, self.others)
65 class Field:
66 "MIME_Type.get_* functions return a list of these."
67 def __init__(self, item):
68 self.item = item
70 def __str__(self):
71 return "<%s>" % self.item
73 def add_subtree(self, model, iter):
74 return
76 def __cmp__(self, b):
77 return cmp(str(self), str(b))
79 class Comment(Field):
80 def __str__(self):
81 lang, data = self.item
82 if lang:
83 lang = '[' + lang + '] '
84 else:
85 lang = '(default) '
86 return lang + data
88 class Glob(Field):
89 def __str__(self):
90 return "Match '%s'" % self.item
92 class Magic(Field):
93 def __str__(self):
94 prio, match = self.item
95 return "Match with priority %s" % prio
97 def add_subtree(self, model, parent):
98 def build(match, parent):
99 for m in match.matches:
100 text = '%s at %s = %s' % (m.type, m.offset, m.value)
101 if m.mask:
102 text += ' masked with ' + m.mask
103 iter = model.append(parent)
104 model.set(iter, 0, text)
105 build(m, iter)
106 build(self.item[1], parent)
108 def __cmp__(self, b):
109 ret = cmp(str(self), str(b))
110 if ret: return ret
111 return cmp(self.item[1], b.item[1])
113 class XML(Field):
114 def __str__(self):
115 return "<%s> with namespace '%s'" % (self.item[1], self.item[0])
117 class Others(Field): pass
119 class FieldParser:
120 def __init__(self, type, attrs):
121 self.type = type
123 def start(self, element, attrs): pass
124 def data(self, data): pass
125 def end(self): pass
127 class CommentParser(FieldParser):
128 def __init__(self, type, attrs):
129 FieldParser.__init__(self, type, attrs)
130 self.lang = attrs.get(XML_NAMESPACE + ' lang', None)
131 self.comment = ''
133 def data(self, data):
134 self.comment += data
136 def end(self):
137 self.type.add_comment(self.lang, self.comment)
139 class Match:
140 def __init__(self, parent):
141 self.parent = parent
142 self.matches = []
144 def __cmp__(self, b):
145 def child_cmp():
146 for x, y in zip(self.matches, b.matches):
147 c = cmp(x, y)
148 if c: return c
149 return 0
151 return cmp(self.type, b.type) or cmp(self.offset, b.offset) or \
152 cmp(self.value, b.value) or cmp(self.mask, b.mask) or \
153 child_cmp()
155 class MagicParser(FieldParser):
156 def __init__(self, type, attrs):
157 FieldParser.__init__(self, type, attrs)
158 self.prio = attrs.get('priority', 50)
159 self.match = Match(None)
161 def start(self, element, attrs):
162 new = Match(self.match)
163 new.offset = attrs.get('offset', '?')
164 new.type = attrs.get('type', '?')
165 new.value = attrs.get('value', '?')
166 new.mask = attrs.get('mask', None)
167 self.match.matches.append(new)
169 def end(self):
170 if self.match.parent:
171 self.match = self.match.parent
172 else:
173 self.type.add_magic(self.prio, self.match)
175 class Scanner:
176 def __init__(self):
177 self.level = 0
178 self.type = None
179 self.handler = None
181 def parse(self, path):
182 parser = expat.ParserCreate(namespace_separator = ' ')
183 parser.StartElementHandler = self.start
184 parser.EndElementHandler = self.end
185 parser.CharacterDataHandler = self.data
186 parser.ParseFile(file(path))
188 def start(self, element, attrs):
189 self.level += 1
190 if self.level == 1:
191 assert element == FREE_NS + ' mime-info'
192 elif self.level == 2:
193 assert element == FREE_NS + ' mime-type'
194 self.type = get_type(attrs['type'])
195 elif self.level == 3:
196 if element == FREE_NS + ' comment':
197 self.handler = CommentParser(self.type, attrs)
198 elif element == FREE_NS + ' glob':
199 self.type.add_glob(attrs['pattern'])
200 elif element == FREE_NS + ' magic':
201 self.handler = MagicParser(self.type, attrs)
202 elif element == FREE_NS + ' root-XML':
203 self.type.add_xml(attrs['namespaceURI'], attrs['localName'])
204 else:
205 self.type.add_other(element)
206 else:
207 assert self.handler
208 self.handler.start(element, attrs)
210 def end(self, element):
211 if self.handler:
212 self.handler.end()
213 self.level -=1
214 if self.level == 1:
215 self.type = None
216 elif self.level == 2:
217 self.handler = None
219 def data(self, data):
220 if self.handler:
221 self.handler.data(data)
223 def scan_file(path):
224 if not path.endswith('.xml'): return
225 if not os.path.exists(path):
226 return
227 scanner = Scanner()
228 try:
229 scanner.parse(path)
230 except:
231 rox.report_exception()
233 def get_override():
234 from xml.dom import minidom
235 if os.path.exists(user_override):
236 doc = minidom.parse(user_override)
237 else:
238 doc = minidom.Document()
239 node = doc.createElementNS(FREE_NS, 'mime-info')
240 doc.appendChild(node)
241 node.setAttributeNS(XMLNS_NAMESPACE, 'xmlns', FREE_NS)
242 return doc
244 def write_override(doc):
245 home_packages = os.path.join(home_mime, 'packages')
246 if not os.path.isdir(home_packages):
247 os.makedirs(home_packages)
248 path = os.path.join(home_packages, 'Override.xml.new')
249 doc.writexml(file(path, 'w'))
250 os.rename(path, path[:-4])
251 r, w = os.pipe()
252 child = os.fork()
253 if child == 0:
254 # Child
255 try:
256 os.close(r)
257 os.dup2(w, 1)
258 os.dup2(w, 2)
259 os.execlp('update-mime-database', 'update-mime-database', home_mime)
260 finally:
261 os._exit(1)
262 os.close(w)
263 rox.info(os.fdopen(r, 'r').read())
264 os.waitpid(child, 0)
266 import __main__
267 __main__.box.update()
269 def add_type(name):
270 doc = get_override()
272 root = doc.documentElement
273 type_node = doc.createElementNS(FREE_NS, 'mime-type')
274 root.appendChild(type_node)
276 type_node.setAttributeNS(None, 'type', name)
278 write_override(doc)
280 def delete_type(name):
281 from xml.dom import Node
282 doc = get_override()
283 removed = False
284 for c in doc.documentElement.childNodes:
285 if c.nodeType != Node.ELEMENT_NODE: continue
286 if c.nodeName != 'mime-type': continue
287 if c.namespaceURI != FREE_NS: continue
288 if c.getAttributeNS(None, 'type') != name: continue
289 doc.documentElement.removeChild(c)
290 removed = True
291 if not removed:
292 rox.alert("No user-provided information about this type -- can't remove anything")
293 return
294 if not rox.confirm("Really remove all user-specified information about type %s?" % name,
295 g.STOCK_DELETE):
296 return
297 write_override(doc)
299 def init():
300 "(Re)read the database."
301 global types
302 types = {}
303 for mime_dir in ['/usr/share/mime', '/usr/local/share/mime', home_mime]:
304 packages_dir = os.path.join(mime_dir, 'packages')
305 if not os.path.isdir(packages_dir):
306 continue
307 packages = os.listdir(packages_dir)
308 packages.sort()
309 for package in packages:
310 if package == 'Override.xml' and mime_dir is home_mime: continue
311 scan_file(os.path.join(packages_dir, package))
312 scan_file(user_override)