summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Kluyver <takowl@gmail.com>2013-05-25 08:32:21 -0700
committerThomas Kluyver <takowl@gmail.com>2013-05-25 08:32:21 -0700
commit1ad37f33877f7f081e695d01d180c826c2589144 (patch)
tree49a9b9a23bca78ae8aadbc5f8d7c510bea4803e8
parentdaca4a71c571e0aa273c4dc9e2600266b486b995 (diff)
parent7fd972e2383a504773203104e30909db77a1298f (diff)
Merge pull request #4 from ju1ius/etree
Port of Menu & Menu editor to ElementTree
-rw-r--r--test/test-menu-rules.py10
-rw-r--r--xdg/Menu.py1431
-rw-r--r--xdg/MenuEditor.py256
3 files changed, 849 insertions, 848 deletions
diff --git a/test/test-menu-rules.py b/test/test-menu-rules.py
index b5fee3b..1bfafb1 100644
--- a/test/test-menu-rules.py
+++ b/test/test-menu-rules.py
@@ -1,8 +1,7 @@
-import xml.dom.minidom
+import xml.etree.cElementTree as etree
import unittest
-from xdg.Menu import Rule
-from xdg.Menu import __parseRule as parseRule
+from xdg.Menu import Parser, Rule
_tests = [
@@ -161,9 +160,10 @@ class RulesTest(unittest.TestCase):
"""Basic rule matching tests"""
def test_rule_from_node(self):
+ parser = Parser(debug=True)
for i, test in enumerate(_tests):
- root = xml.dom.minidom.parseString(test['doc']).childNodes[0]
- rule = parseRule(root)
+ root = etree.fromstring(test['doc'])
+ rule = parser.parse_rule(root)
for j, data in enumerate(test['data']):
menuentry = MockMenuEntry(data[0], data[1])
result = eval(rule.code)
diff --git a/xdg/Menu.py b/xdg/Menu.py
index 2734554..ba5fe97 100644
--- a/xdg/Menu.py
+++ b/xdg/Menu.py
@@ -17,35 +17,38 @@ def print_menu(menu, tab=0):
print_menu(parse())
"""
-import locale, os, xml.dom.minidom
+import os
+import locale
import subprocess
import ast
+try:
+ import xml.etree.cElementTree as etree
+except ImportError:
+ import xml.etree.ElementTree as etree
from xdg.BaseDirectory import xdg_data_dirs, xdg_config_dirs
from xdg.DesktopEntry import DesktopEntry
-from xdg.Exceptions import ParsingError, ValidationError, debug
+from xdg.Exceptions import ParsingError, debug
from xdg.util import PY3
import xdg.Locale
import xdg.Config
-ELEMENT_NODE = xml.dom.Node.ELEMENT_NODE
-TEXT_NODE = xml.dom.Node.TEXT_NODE
-CDATA_SECTION_NODE = xml.dom.Node.CDATA_SECTION_NODE
def _strxfrm(s):
"""Wrapper around locale.strxfrm that accepts unicode strings on Python 2.
-
+
See Python bug #2481.
"""
if (not PY3) and isinstance(s, unicode):
s = s.encode('utf-8')
return locale.strxfrm(s)
+
class Menu:
"""Menu containing sub menus under menu.Entries
- Contains both Menu and MenuEntry items.
+ Contains both Menu and MenuEntry items.
"""
def __init__(self):
# Public stuff
@@ -114,11 +117,11 @@ class Menu:
# FIXME: Performance: cache getName()
def __cmp__(self, other):
return locale.strcoll(self.getName(), other.getName())
-
+
def _key(self):
"""Key function for locale-aware sorting."""
return _strxfrm(self.getName())
-
+
def __lt__(self, other):
try:
other = other._key()
@@ -136,21 +139,21 @@ class Menu:
def getEntries(self, hidden=False):
"""Interator for a list of Entries visible to the user."""
for entry in self.Entries:
- if hidden == True:
+ if hidden:
yield entry
- elif entry.Show == True:
+ elif entry.Show:
yield entry
# FIXME: Add searchEntry/seaqrchMenu function
# search for name/comment/genericname/desktopfileide
# return multiple items
- def getMenuEntry(self, desktopfileid, deep = False):
+ def getMenuEntry(self, desktopfileid, deep=False):
"""Searches for a MenuEntry with a given DesktopFileID."""
for menuentry in self.MenuEntries:
if menuentry.DesktopFileID == desktopfileid:
return menuentry
- if deep == True:
+ if deep:
for submenu in self.Submenus:
submenu.getMenuEntry(desktopfileid, deep)
@@ -167,7 +170,7 @@ class Menu:
def getPath(self, org=False, toplevel=False):
"""Returns this menu's path in the menu structure."""
parent = self
- names=[]
+ names = []
while 1:
if org:
names.append(parent.Name)
@@ -179,7 +182,7 @@ class Menu:
break
names.reverse()
path = ""
- if toplevel == False:
+ if not toplevel:
names.pop(0)
for name in names:
path = os.path.join(path, name)
@@ -213,6 +216,107 @@ class Menu:
except AttributeError:
return ""
+ def sort(self):
+ self.Entries = []
+ self.Visible = 0
+
+ for submenu in self.Submenus:
+ submenu.sort()
+
+ _submenus = set()
+ _entries = set()
+
+ for order in self.Layout.order:
+ if order[0] == "Filename":
+ _entries.add(order[1])
+ elif order[0] == "Menuname":
+ _submenus.add(order[1])
+
+ for order in self.Layout.order:
+ if order[0] == "Separator":
+ separator = Separator(self)
+ if len(self.Entries) > 0 and isinstance(self.Entries[-1], Separator):
+ separator.Show = False
+ self.Entries.append(separator)
+ elif order[0] == "Filename":
+ menuentry = self.getMenuEntry(order[1])
+ if menuentry:
+ self.Entries.append(menuentry)
+ elif order[0] == "Menuname":
+ submenu = self.getMenu(order[1])
+ if submenu:
+ if submenu.Layout.inline:
+ self.merge_inline(submenu)
+ else:
+ self.Entries.append(submenu)
+ elif order[0] == "Merge":
+ if order[1] == "files" or order[1] == "all":
+ self.MenuEntries.sort()
+ for menuentry in self.MenuEntries:
+ if menuentry.DesktopFileID not in _entries:
+ self.Entries.append(menuentry)
+ elif order[1] == "menus" or order[1] == "all":
+ self.Submenus.sort()
+ for submenu in self.Submenus:
+ if submenu.Name not in _submenus:
+ if submenu.Layout.inline:
+ self.merge_inline(submenu)
+ else:
+ self.Entries.append(submenu)
+
+ # getHidden / NoDisplay / OnlyShowIn / NotOnlyShowIn / Deleted / NoExec
+ for entry in self.Entries:
+ entry.Show = True
+ self.Visible += 1
+ if isinstance(entry, Menu):
+ if entry.Deleted is True:
+ entry.Show = "Deleted"
+ self.Visible -= 1
+ elif isinstance(entry.Directory, MenuEntry):
+ if entry.Directory.DesktopEntry.getNoDisplay():
+ entry.Show = "NoDisplay"
+ self.Visible -= 1
+ elif entry.Directory.DesktopEntry.getHidden():
+ entry.Show = "Hidden"
+ self.Visible -= 1
+ elif isinstance(entry, MenuEntry):
+ if entry.DesktopEntry.getNoDisplay():
+ entry.Show = "NoDisplay"
+ self.Visible -= 1
+ elif entry.DesktopEntry.getHidden():
+ entry.Show = "Hidden"
+ self.Visible -= 1
+ elif entry.DesktopEntry.getTryExec() and not entry.DesktopEntry.findTryExec():
+ entry.Show = "NoExec"
+ self.Visible -= 1
+ elif xdg.Config.windowmanager:
+ if (entry.DesktopEntry.OnlyShowIn != [] and (
+ xdg.Config.windowmanager not in entry.DesktopEntry.OnlyShowIn
+ )
+ ) or (
+ xdg.Config.windowmanager in entry.DesktopEntry.NotShowIn
+ ):
+ entry.Show = "NotShowIn"
+ self.Visible -= 1
+ elif isinstance(entry, Separator):
+ self.Visible -= 1
+
+ # remove separators at the beginning and at the end
+ if len(self.Entries) > 0:
+ if isinstance(self.Entries[0], Separator):
+ self.Entries[0].Show = False
+ if len(self.Entries) > 1:
+ if isinstance(self.Entries[-1], Separator):
+ self.Entries[-1].Show = False
+
+ # show_empty tag
+ for entry in self.Entries[:]:
+ if isinstance(entry, Menu) and not entry.Layout.show_empty and entry.Visible == 0:
+ entry.Show = "Empty"
+ self.Visible -= 1
+ if entry.NotInXml is True:
+ self.Entries.remove(entry)
+
""" PRIVATE STUFF """
def addSubmenu(self, newmenu):
for submenu in self.Submenus:
@@ -224,95 +328,59 @@ class Menu:
newmenu.Parent = self
newmenu.Depth = self.Depth + 1
+ # inline tags
+ def merge_inline(self, submenu):
+ """Appends a submenu's entries to this menu
+ See the <Menuname> section of the spec about the "inline" attribute
+ """
+ if len(submenu.Entries) == 1 and submenu.Layout.inline_alias:
+ menuentry = submenu.Entries[0]
+ menuentry.DesktopEntry.set("Name", submenu.getName(), locale=True)
+ menuentry.DesktopEntry.set("GenericName", submenu.getGenericName(), locale=True)
+ menuentry.DesktopEntry.set("Comment", submenu.getComment(), locale=True)
+ self.Entries.append(menuentry)
+ elif len(submenu.Entries) <= submenu.Layout.inline_limit or submenu.Layout.inline_limit == 0:
+ if submenu.Layout.inline_header:
+ header = Header(submenu.getName(), submenu.getGenericName(), submenu.getComment())
+ self.Entries.append(header)
+ for entry in submenu.Entries:
+ self.Entries.append(entry)
+ else:
+ self.Entries.append(submenu)
+
+
class Move:
"A move operation"
- def __init__(self, node=None):
- if node:
- self.parseNode(node)
- else:
- self.Old = ""
- self.New = ""
+ def __init__(self, old="", new=""):
+ self.Old = old
+ self.New = new
def __cmp__(self, other):
return cmp(self.Old, other.Old)
- def parseNode(self, node):
- for child in node.childNodes:
- if child.nodeType == ELEMENT_NODE:
- if child.tagName == "Old":
- try:
- self.parseOld(child.childNodes[0].nodeValue)
- except IndexError:
- raise ValidationError('Old cannot be empty', '??')
- elif child.tagName == "New":
- try:
- self.parseNew(child.childNodes[0].nodeValue)
- except IndexError:
- raise ValidationError('New cannot be empty', '??')
-
- def parseOld(self, value):
- self.Old = value
- def parseNew(self, value):
- self.New = value
-
class Layout:
"Menu Layout class"
- def __init__(self, node=None):
- self.order = []
- if node:
- self.show_empty = node.getAttribute("show_empty") or "false"
- self.inline = node.getAttribute("inline") or "false"
- self.inline_limit = node.getAttribute("inline_limit") or 4
- self.inline_header = node.getAttribute("inline_header") or "true"
- self.inline_alias = node.getAttribute("inline_alias") or "false"
- self.inline_limit = int(self.inline_limit)
- self.parseNode(node)
- else:
- self.show_empty = "false"
- self.inline = "false"
- self.inline_limit = 4
- self.inline_header = "true"
- self.inline_alias = "false"
- self.order.append(["Merge", "menus"])
- self.order.append(["Merge", "files"])
-
- def parseNode(self, node):
- for child in node.childNodes:
- if child.nodeType == ELEMENT_NODE:
- if child.tagName == "Menuname":
- try:
- self.parseMenuname(
- child.childNodes[0].nodeValue,
- child.getAttribute("show_empty") or "false",
- child.getAttribute("inline") or "false",
- child.getAttribute("inline_limit") or 4,
- child.getAttribute("inline_header") or "true",
- child.getAttribute("inline_alias") or "false")
- except IndexError:
- raise ValidationError('Menuname cannot be empty', "")
- elif child.tagName == "Separator":
- self.parseSeparator()
- elif child.tagName == "Filename":
- try:
- self.parseFilename(child.childNodes[0].nodeValue)
- except IndexError:
- raise ValidationError('Filename cannot be empty', "")
- elif child.tagName == "Merge":
- self.parseMerge(child.getAttribute("type") or "all")
-
- def parseMenuname(self, value, empty="false", inline="false", inline_limit=4, inline_header="true", inline_alias="false"):
- self.order.append(["Menuname", value, empty, inline, inline_limit, inline_header, inline_alias])
- self.order[-1][4] = int(self.order[-1][4])
-
- def parseSeparator(self):
- self.order.append(["Separator"])
-
- def parseFilename(self, value):
- self.order.append(["Filename", value])
-
- def parseMerge(self, type="all"):
- self.order.append(["Merge", type])
+ def __init__(self, show_empty=False, inline=False, inline_limit=4,
+ inline_header=True, inline_alias=False):
+ self.show_empty = show_empty
+ self.inline = inline
+ self.inline_limit = inline_limit
+ self.inline_header = inline_header
+ self.inline_alias = inline_alias
+ self._order = []
+ self._default_order = [
+ ['Merge', 'menus'],
+ ['Merge', 'files']
+ ]
+
+ @property
+ def order(self):
+ return self._order if self._order else self._default_order
+
+ @order.setter
+ def order(self, order):
+ self._order = order
class Rule:
@@ -364,9 +432,14 @@ class Rule:
class MenuEntry:
"Wrapper for 'Menu Style' Desktop Entries"
+
+ TYPE_USER = "User"
+ TYPE_SYSTEM = "System"
+ TYPE_BOTH = "Both"
+
def __init__(self, filename, dir="", prefix=""):
# Create entry
- self.DesktopEntry = DesktopEntry(os.path.join(dir,filename))
+ self.DesktopEntry = DesktopEntry(os.path.join(dir, filename))
self.setAttributes(filename, dir, prefix)
# Can be one of Deleted/Hidden/Empty/NotShowIn/NoExec or True
@@ -386,7 +459,7 @@ class MenuEntry:
def save(self):
"""Save any changes to the desktop entry."""
- if self.DesktopEntry.tainted == True:
+ if self.DesktopEntry.tainted:
self.DesktopEntry.write()
def getDir(self):
@@ -395,20 +468,20 @@ class MenuEntry:
def getType(self):
"""Return the type of MenuEntry, System/User/Both"""
- if xdg.Config.root_mode == False:
+ if not xdg.Config.root_mode:
if self.Original:
- return "Both"
+ return self.TYPE_BOTH
elif xdg_data_dirs[0] in self.DesktopEntry.filename:
- return "User"
+ return self.TYPE_USER
else:
- return "System"
+ return self.TYPE_SYSTEM
else:
- return "User"
+ return self.TYPE_USER
def setAttributes(self, filename, dir="", prefix=""):
self.Filename = filename
self.Prefix = prefix
- self.DesktopFileID = os.path.join(prefix,filename).replace("/", "-")
+ self.DesktopFileID = os.path.join(prefix, filename).replace("/", "-")
if not os.path.isabs(self.DesktopEntry.filename):
self.__setFilename()
@@ -419,32 +492,31 @@ class MenuEntry:
self.__setFilename()
def __setFilename(self):
- if xdg.Config.root_mode == False:
+ if not xdg.Config.root_mode:
path = xdg_data_dirs[0]
else:
- path= xdg_data_dirs[1]
+ path = xdg_data_dirs[1]
if self.DesktopEntry.getType() == "Application":
- dir = os.path.join(path, "applications")
+ dir_ = os.path.join(path, "applications")
else:
- dir = os.path.join(path, "desktop-directories")
+ dir_ = os.path.join(path, "desktop-directories")
- self.DesktopEntry.filename = os.path.join(dir, self.Filename)
+ self.DesktopEntry.filename = os.path.join(dir_, self.Filename)
def __cmp__(self, other):
return locale.strcoll(self.DesktopEntry.getName(), other.DesktopEntry.getName())
-
+
def _key(self):
"""Key function for locale-aware sorting."""
return _strxfrm(self.DesktopEntry.getName())
-
+
def __lt__(self, other):
try:
other = other._key()
except AttributeError:
pass
return self._key() < other
-
def __eq__(self, other):
if self.DesktopFileID == str(other):
@@ -473,629 +545,519 @@ class Header:
def __str__(self):
return self.Name
-# Some XML utility functions
-
-def _get_children(node):
- return [n for n in node.childNodes if n.nodeType == ELEMENT_NODE]
+TYPE_DIR, TYPE_FILE = 0, 1
-def _iter_children(node):
- for n in node.childNodes:
- if n.nodeType == ELEMENT_NODE:
- yield n
+def _check_file_path(value, filename, type):
+ path = os.path.dirname(filename)
+ if not os.path.isabs(value):
+ value = os.path.join(path, value)
+ value = os.path.abspath(value)
+ if not os.path.exists(value):
+ return False
+ if type == TYPE_DIR and os.path.isdir(value):
+ return value
+ if type == TYPE_FILE and os.path.isfile(value):
+ return value
+ return False
-def _get_node_text(node):
- return ' '.join([
- n.nodeValue.strip() for n in node.childNodes if n.nodeType in (TEXT_NODE, CDATA_SECTION_NODE)
- ]).strip()
+def _get_menu_file_path(filename):
+ dirs = list(xdg_config_dirs)
+ if xdg.Config.root_mode is True:
+ dirs.pop(0)
+ for d in dirs:
+ menuname = os.path.join(d, "menus", filename)
+ if os.path.isfile(menuname):
+ return menuname
-tmp = {}
+def _to_bool(value):
+ if isinstance(value, bool):
+ return value
+ return value.lower() == "true"
-def __getFileName(filename):
- dirs = xdg_config_dirs[:]
- if xdg.Config.root_mode == True:
- dirs.pop(0)
- for dir in dirs:
- menuname = os.path.join (dir, "menus" , filename)
- if os.path.isdir(dir) and os.path.isfile(menuname):
- return menuname
+# remove duplicate entries from a list
+def _dedupe(_list):
+ _set = {}
+ _list.reverse()
+ _list = [_set.setdefault(e, e) for e in _list if e not in _set]
+ _list.reverse()
+ return _list
+
+
+class Parser(object):
+
+ def __init__(self, debug=False):
+ self.debug = debug
+
+ def parse(self, filename=None):
+ """Load an applications.menu file.
+
+ filename : str, optional
+ The default is ``$XDG_CONFIG_DIRS/menus/${XDG_MENU_PREFIX}applications.menu``.
+ """
+ # convert to absolute path
+ if filename and not os.path.isabs(filename):
+ filename = _get_menu_file_path(filename)
+ # use default if no filename given
+ if not filename:
+ candidate = os.environ.get('XDG_MENU_PREFIX', '') + "applications.menu"
+ filename = _get_menu_file_path(candidate)
+ if not filename:
+ raise ParsingError('File not found', "/etc/xdg/menus/%s" % candidate)
+ # check if it is a .menu file
+ if not filename.endswith(".menu"):
+ raise ParsingError('Not a .menu file', filename)
+ # create xml parser
+ try:
+ tree = etree.parse(filename)
+ except:
+ raise ParsingError('Not a valid .menu file', filename)
-def parse(filename=None):
- """Load an applications.menu file.
-
- filename : str, optional
- The default is ``$XDG_CONFIG_DIRS/menus/${XDG_MENU_PREFIX}applications.menu``.
- """
- # convert to absolute path
- if filename and not os.path.isabs(filename):
- filename = __getFileName(filename)
-
- # use default if no filename given
- if not filename:
- candidate = os.environ.get('XDG_MENU_PREFIX', '') + "applications.menu"
- filename = __getFileName(candidate)
-
- if not filename:
- raise ParsingError('File not found', "/etc/xdg/menus/%s" % candidate)
-
- # check if it is a .menu file
- if not os.path.splitext(filename)[1] == ".menu":
- raise ParsingError('Not a .menu file', filename)
-
- # create xml parser
- try:
- doc = xml.dom.minidom.parse(filename)
- except xml.parsers.expat.ExpatError:
- raise ParsingError('Not a valid .menu file', filename)
-
- # parse menufile
- tmp["Root"] = ""
- tmp["mergeFiles"] = []
- tmp["DirectoryDirs"] = []
- tmp["cache"] = MenuEntryCache()
-
- __parse(doc, filename, tmp["Root"])
- __parsemove(tmp["Root"])
- __postparse(tmp["Root"])
-
- tmp["Root"].Doc = doc
- tmp["Root"].Filename = filename
-
- # generate the menu
- __genmenuNotOnlyAllocated(tmp["Root"])
- __genmenuOnlyAllocated(tmp["Root"])
-
- # and finally sort
- sort(tmp["Root"])
-
- return tmp["Root"]
-
-
-def __parse(node, filename, parent=None):
- for child in node.childNodes:
- if child.nodeType == ELEMENT_NODE:
- if child.tagName == 'Menu':
- __parseMenu(child, filename, parent)
- elif child.tagName == 'AppDir':
- try:
- __parseAppDir(child.childNodes[0].nodeValue, filename, parent)
- except IndexError:
- raise ValidationError('AppDir cannot be empty', filename)
- elif child.tagName == 'DefaultAppDirs':
- __parseDefaultAppDir(filename, parent)
- elif child.tagName == 'DirectoryDir':
- try:
- __parseDirectoryDir(child.childNodes[0].nodeValue, filename, parent)
- except IndexError:
- raise ValidationError('DirectoryDir cannot be empty', filename)
- elif child.tagName == 'DefaultDirectoryDirs':
- __parseDefaultDirectoryDir(filename, parent)
- elif child.tagName == 'Name' :
- try:
- parent.Name = child.childNodes[0].nodeValue
- except IndexError:
- raise ValidationError('Name cannot be empty', filename)
- elif child.tagName == 'Directory' :
- try:
- parent.Directories.append(child.childNodes[0].nodeValue)
- except IndexError:
- raise ValidationError('Directory cannot be empty', filename)
- elif child.tagName == 'OnlyUnallocated':
+ # parse menufile
+ self._merged_files = set()
+ self._directory_dirs = set()
+ self.cache = MenuEntryCache()
+
+ menu = self.parse_menu(tree.getroot(), filename)
+ menu.tree = tree
+ menu.filename = filename
+
+ self.handle_moves(menu)
+ self.post_parse(menu)
+
+ # generate the menu
+ self.generate_not_only_allocated(menu)
+ self.generate_only_allocated(menu)
+
+ # and finally sort
+ menu.sort()
+
+ return menu
+
+ def parse_menu(self, node, filename):
+ menu = Menu()
+ self.parse_node(node, filename, menu)
+ return menu
+
+ def parse_node(self, node, filename, parent=None):
+ num_children = len(node)
+ for child in node:
+ tag, text = child.tag, child.text
+ text = text.strip() if text else None
+ if tag == 'Menu':
+ menu = self.parse_menu(child, filename)
+ parent.addSubmenu(menu)
+ elif tag == 'AppDir' and text:
+ self.parse_app_dir(text, filename, parent)
+ elif tag == 'DefaultAppDirs':
+ self.parse_default_app_dir(filename, parent)
+ elif tag == 'DirectoryDir' and text:
+ self.parse_directory_dir(text, filename, parent)
+ elif tag == 'DefaultDirectoryDirs':
+ self.parse_default_directory_dir(filename, parent)
+ elif tag == 'Name' and text:
+ parent.Name = text
+ elif tag == 'Directory' and text:
+ parent.Directories.append(text)
+ elif tag == 'OnlyUnallocated':
parent.OnlyUnallocated = True
- elif child.tagName == 'NotOnlyUnallocated':
+ elif tag == 'NotOnlyUnallocated':
parent.OnlyUnallocated = False
- elif child.tagName == 'Deleted':
+ elif tag == 'Deleted':
parent.Deleted = True
- elif child.tagName == 'NotDeleted':
+ elif tag == 'NotDeleted':
parent.Deleted = False
- elif child.tagName == 'Include' or child.tagName == 'Exclude':
- parent.Rules.append(__parseRule(child))
- elif child.tagName == 'MergeFile':
- try:
- if child.getAttribute("type") == "parent":
- __parseMergeFile("applications.menu", child, filename, parent)
- else:
- __parseMergeFile(child.childNodes[0].nodeValue, child, filename, parent)
- except IndexError:
- raise ValidationError('MergeFile cannot be empty', filename)
- elif child.tagName == 'MergeDir':
- try:
- __parseMergeDir(child.childNodes[0].nodeValue, child, filename, parent)
- except IndexError:
- raise ValidationError('MergeDir cannot be empty', filename)
- elif child.tagName == 'DefaultMergeDirs':
- __parseDefaultMergeDirs(child, filename, parent)
- elif child.tagName == 'Move':
- parent.Moves.append(Move(child))
- elif child.tagName == 'Layout':
- if len(child.childNodes) > 1:
- parent.Layout = Layout(child)
- elif child.tagName == 'DefaultLayout':
- if len(child.childNodes) > 1:
- parent.DefaultLayout = Layout(child)
- elif child.tagName == 'LegacyDir':
- try:
- __parseLegacyDir(child.childNodes[0].nodeValue, child.getAttribute("prefix"), filename, parent)
- except IndexError:
- raise ValidationError('LegacyDir cannot be empty', filename)
- elif child.tagName == 'KDELegacyDirs':
- __parseKDELegacyDirs(filename, parent)
-
-def __parsemove(menu):
- for submenu in menu.Submenus:
- __parsemove(submenu)
-
- # parse move operations
- for move in menu.Moves:
- move_from_menu = menu.getMenu(move.Old)
- if move_from_menu:
- move_to_menu = menu.getMenu(move.New)
-
- menus = move.New.split("/")
- oldparent = None
- while len(menus) > 0:
- if not oldparent:
- oldparent = menu
- newmenu = oldparent.getMenu(menus[0])
- if not newmenu:
- newmenu = Menu()
- newmenu.Name = menus[0]
- if len(menus) > 1:
- newmenu.NotInXml = True
- oldparent.addSubmenu(newmenu)
- oldparent = newmenu
- menus.pop(0)
-
- newmenu += move_from_menu
- move_from_menu.Parent.Submenus.remove(move_from_menu)
-
-def __postparse(menu):
- # unallocated / deleted
- if menu.Deleted == "notset":
- menu.Deleted = False
- if menu.OnlyUnallocated == "notset":
- menu.OnlyUnallocated = False
-
- # Layout Tags
- if not menu.Layout or not menu.DefaultLayout:
- if menu.DefaultLayout:
- menu.Layout = menu.DefaultLayout
- elif menu.Layout:
- if menu.Depth > 0:
- menu.DefaultLayout = menu.Parent.DefaultLayout
- else:
- menu.DefaultLayout = Layout()
+ elif tag == 'Include' or tag == 'Exclude':
+ parent.Rules.append(self.parse_rule(child))
+ elif tag == 'MergeFile':
+ if child.attrib.get("type", None) == "parent":
+ self.parse_merge_file("applications.menu", child, filename, parent)
+ elif text:
+ self.parse_merge_file(text, child, filename, parent)
+ elif tag == 'MergeDir' and text:
+ self.parse_merge_dir(text, child, filename, parent)
+ elif tag == 'DefaultMergeDirs':
+ self.parse_default_merge_dirs(child, filename, parent)
+ elif tag == 'Move':
+ parent.Moves.append(self.parse_move(child))
+ elif tag == 'Layout':
+ if num_children > 1:
+ parent.Layout = self.parse_layout(child)
+ elif tag == 'DefaultLayout':
+ if num_children > 1:
+ parent.DefaultLayout = self.parse_layout(child)
+ elif tag == 'LegacyDir' and text:
+ self.parse_legacy_dir(text, child.attrib.get("prefix", ""), filename, parent)
+ elif tag == 'KDELegacyDirs':
+ self.parse_kde_legacy_dirs(filename, parent)
+
+ def parse_layout(self, node):
+ layout = Layout(
+ show_empty=_to_bool(node.attrib.get("show_empty", False)),
+ inline=_to_bool(node.attrib.get("inline", False)),
+ inline_limit=int(node.attrib.get("inline_limit", 4)),
+ inline_header=_to_bool(node.attrib.get("inline_header", True)),
+ inline_alias=_to_bool(node.attrib.get("inline_alias", False))
+ )
+ for child in node:
+ tag, text = child.tag, child.text
+ text = text.strip() if text else None
+ if tag == "Menuname" and text:
+ layout.order.append([
+ "Menuname",
+ text,
+ _to_bool(child.attrib.get("show_empty", False)),
+ _to_bool(child.attrib.get("inline", False)),
+ int(child.attrib.get("inline_limit", 4)),
+ _to_bool(child.attrib.get("inline_header", True)),
+ _to_bool(child.attrib.get("inline_alias", False))
+ ])
+ elif tag == "Separator":
+ layout.order.append(['Separator'])
+ elif tag == "Filename" and text:
+ layout.order.append(["Filename", text])
+ elif tag == "Merge":
+ layout.order.append([
+ "Merge",
+ child.attrib.get("type", "all")
+ ])
+ return layout
+
+ def parse_move(self, node):
+ old, new = "", ""
+ for child in node:
+ tag, text = child.tag, child.text
+ text = text.strip() if text else None
+ if tag == "Old" and text:
+ old = text
+ elif tag == "New" and text:
+ new = text
+ return Move(old, new)
+
+ # ---------- <Rule> parsing
+
+ def parse_rule(self, node):
+ type = Rule.TYPE_INCLUDE if node.tag == 'Include' else Rule.TYPE_EXCLUDE
+ tree = ast.Expression(lineno=1, col_offset=0)
+ expr = self.parse_bool_op(node, ast.Or())
+ if expr:
+ tree.body = expr
else:
- if menu.Depth > 0:
- menu.Layout = menu.Parent.DefaultLayout
- menu.DefaultLayout = menu.Parent.DefaultLayout
- else:
- menu.Layout = Layout()
- menu.DefaultLayout = Layout()
-
- # add parent's app/directory dirs
- if menu.Depth > 0:
- menu.AppDirs = menu.Parent.AppDirs + menu.AppDirs
- menu.DirectoryDirs = menu.Parent.DirectoryDirs + menu.DirectoryDirs
-
- # remove duplicates
- menu.Directories = __removeDuplicates(menu.Directories)
- menu.DirectoryDirs = __removeDuplicates(menu.DirectoryDirs)
- menu.AppDirs = __removeDuplicates(menu.AppDirs)
-
- # go recursive through all menus
- for submenu in menu.Submenus:
- __postparse(submenu)
-
- # reverse so handling is easier
- menu.Directories.reverse()
- menu.DirectoryDirs.reverse()
- menu.AppDirs.reverse()
-
- # get the valid .directory file out of the list
- for directory in menu.Directories:
- for dir in menu.DirectoryDirs:
- if os.path.isfile(os.path.join(dir, directory)):
- menuentry = MenuEntry(directory, dir)
- if not menu.Directory:
- menu.Directory = menuentry
- elif menuentry.getType() == "System":
- if menu.Directory.getType() == "User":
- menu.Directory.Original = menuentry
- if menu.Directory:
- break
+ tree.body = ast.Name('False', ast.Load())
+ ast.fix_missing_locations(tree)
+ return Rule(type, tree)
+
+ def parse_bool_op(self, node, operator):
+ values = []
+ for child in node:
+ rule = self.parse_rule_node(child)
+ if rule:
+ values.append(rule)
+ num_values = len(values)
+ if num_values > 1:
+ return ast.BoolOp(operator, values)
+ elif num_values == 1:
+ return values[0]
+ return None
+
+ def parse_rule_node(self, node):
+ tag = node.tag
+ if tag == 'Or':
+ return self.parse_bool_op(node, ast.Or())
+ elif tag == 'And':
+ return self.parse_bool_op(node, ast.And())
+ elif tag == 'Not':
+ expr = self.parse_bool_op(node, ast.Or())
+ return ast.UnaryOp(ast.Not(), expr) if expr else None
+ elif tag == 'All':
+ return ast.Name('True', ast.Load())
+ elif tag == 'Category':
+ category = node.text
+ return ast.Compare(
+ left=ast.Str(category),
+ ops=[ast.In()],
+ comparators=[ast.Attribute(
+ value=ast.Name(id='menuentry', ctx=ast.Load()),
+ attr='Categories',
+ ctx=ast.Load()
+ )]
+ )
+ elif tag == 'Filename':
+ filename = node.text
+ return ast.Compare(
+ left=ast.Str(filename),
+ ops=[ast.Eq()],
+ comparators=[ast.Attribute(
+ value=ast.Name(id='menuentry', ctx=ast.Load()),
+ attr='DesktopFileID',
+ ctx=ast.Load()
+ )]
+ )
+ # ---------- App/Directory Dir Stuff
-# Menu parsing stuff
-def __parseMenu(child, filename, parent):
- m = Menu()
- __parse(child, filename, m)
- if parent:
- parent.addSubmenu(m)
- else:
- tmp["Root"] = m
+ def parse_app_dir(self, value, filename, parent):
+ value = _check_file_path(value, filename, TYPE_DIR)
+ if value:
+ parent.AppDirs.append(value)
-# helper function
-def __check(value, filename, type):
- path = os.path.dirname(filename)
+ def parse_default_app_dir(self, filename, parent):
+ for d in reversed(xdg_data_dirs):
+ self.parse_app_dir(os.path.join(d, "applications"), filename, parent)
- if not os.path.isabs(value):
- value = os.path.join(path, value)
+ def parse_directory_dir(self, value, filename, parent):
+ value = _check_file_path(value, filename, TYPE_DIR)
+ if value:
+ parent.DirectoryDirs.append(value)
+
+ def parse_default_directory_dir(self, filename, parent):
+ for d in reversed(xdg_data_dirs):
+ self.parse_directory_dir(os.path.join(d, "desktop-directories"), filename, parent)
+
+ # ---------- Merge Stuff
+
+ def parse_merge_file(self, value, child, filename, parent):
+ if child.attrib.get("type", None) == "parent":
+ for d in xdg_config_dirs:
+ rel_file = filename.replace(d, "").strip("/")
+ if rel_file != filename:
+ for p in xdg_config_dirs:
+ if d == p:
+ continue
+ if os.path.isfile(os.path.join(p, rel_file)):
+ self.merge_file(os.path.join(p, rel_file), child, parent)
+ break
+ else:
+ value = _check_file_path(value, filename, TYPE_FILE)
+ if value:
+ self.merge_file(value, child, parent)
- value = os.path.abspath(value)
+ def parse_merge_dir(self, value, child, filename, parent):
+ value = _check_file_path(value, filename, TYPE_DIR)
+ if value:
+ for item in os.listdir(value):
+ try:
+ if item.endswith(".menu"):
+ self.merge_file(os.path.join(value, item), child, parent)
+ except UnicodeDecodeError:
+ continue
- if type == "dir" and os.path.exists(value) and os.path.isdir(value):
- return value
- elif type == "file" and os.path.exists(value) and os.path.isfile(value):
- return value
- else:
- return False
+ def parse_default_merge_dirs(self, child, filename, parent):
+ basename = os.path.splitext(os.path.basename(filename))[0]
+ for d in reversed(xdg_config_dirs):
+ self.parse_merge_dir(os.path.join(d, "menus", basename + "-merged"), child, filename, parent)
-# App/Directory Dir Stuff
-def __parseAppDir(value, filename, parent):
- value = __check(value, filename, "dir")
- if value:
- parent.AppDirs.append(value)
-
-def __parseDefaultAppDir(filename, parent):
- for dir in reversed(xdg_data_dirs):
- __parseAppDir(os.path.join(dir, "applications"), filename, parent)
-
-def __parseDirectoryDir(value, filename, parent):
- value = __check(value, filename, "dir")
- if value:
- parent.DirectoryDirs.append(value)
-
-def __parseDefaultDirectoryDir(filename, parent):
- for dir in reversed(xdg_data_dirs):
- __parseDirectoryDir(os.path.join(dir, "desktop-directories"), filename, parent)
-
-# Merge Stuff
-def __parseMergeFile(value, child, filename, parent):
- if child.getAttribute("type") == "parent":
- for dir in xdg_config_dirs:
- rel_file = filename.replace(dir, "").strip("/")
- if rel_file != filename:
- for p in xdg_config_dirs:
- if dir == p:
- continue
- if os.path.isfile(os.path.join(p,rel_file)):
- __mergeFile(os.path.join(p,rel_file),child,parent)
- break
- else:
- value = __check(value, filename, "file")
- if value:
- __mergeFile(value, child, parent)
-
-def __parseMergeDir(value, child, filename, parent):
- value = __check(value, filename, "dir")
- if value:
- for item in os.listdir(value):
- try:
- if os.path.splitext(item)[1] == ".menu":
- __mergeFile(os.path.join(value, item), child, parent)
- except UnicodeDecodeError:
- continue
+ def merge_file(self, filename, child, parent):
+ # check for infinite loops
+ if filename in self._merged_files:
+ if debug:
+ raise ParsingError('Infinite MergeFile loop detected', filename)
+ else:
+ return
+ self._merged_files.add(filename)
+ # load file
+ try:
+ tree = etree.parse(filename)
+ except IOError:
+ if self.debug:
+ raise ParsingError('File not found', filename)
+ else:
+ return
+ except:
+ if self.debug:
+ raise ParsingError('Not a valid .menu file', filename)
+ else:
+ return
+ root = tree.getroot()
+ # append file
+ for child in root:
+ self.parse_node(child, filename, parent)
+ break
-def __parseDefaultMergeDirs(child, filename, parent):
- basename = os.path.splitext(os.path.basename(filename))[0]
- for dir in reversed(xdg_config_dirs):
- __parseMergeDir(os.path.join(dir, "menus", basename + "-merged"), child, filename, parent)
+ # ---------- Legacy Dir Stuff
-def __mergeFile(filename, child, parent):
- # check for infinite loops
- if filename in tmp["mergeFiles"]:
- if debug:
- raise ParsingError('Infinite MergeFile loop detected', filename)
- else:
- return
+ def parse_legacy_dir(self, dir_, prefix, filename, parent):
+ m = self.merge_legacy_dir(dir_, prefix, filename, parent)
+ if m:
+ parent += m
- tmp["mergeFiles"].append(filename)
+ def merge_legacy_dir(self, dir_, prefix, filename, parent):
+ dir_ = _check_file_path(dir_, filename, TYPE_DIR)
+ if dir_ and dir_ not in self._directory_dirs:
+ self._directory_dirs.add(dir_)
+ m = Menu()
+ m.AppDirs.append(dir_)
+ m.DirectoryDirs.append(dir_)
+ m.Name = os.path.basename(dir_)
+ m.NotInXml = True
- # load file
- try:
- doc = xml.dom.minidom.parse(filename)
- except IOError:
- if debug:
- raise ParsingError('File not found', filename)
- else:
- return
- except xml.parsers.expat.ExpatError:
- if debug:
- raise ParsingError('Not a valid .menu file', filename)
- else:
- return
+ for item in os.listdir(dir_):
+ try:
+ if item == ".directory":
+ m.Directories.append(item)
+ elif os.path.isdir(os.path.join(dir_, item)):
+ m.addSubmenu(self.merge_legacy_dir(
+ os.path.join(dir_, item),
+ prefix,
+ filename,
+ parent
+ ))
+ except UnicodeDecodeError:
+ continue
- # append file
- for child in doc.childNodes:
- if child.nodeType == ELEMENT_NODE:
- __parse(child,filename,parent)
- break
+ self.cache.add_menu_entries([dir_], prefix, True)
+ menuentries = self.cache.get_menu_entries([dir_], False)
-# Legacy Dir Stuff
-def __parseLegacyDir(dir, prefix, filename, parent):
- m = __mergeLegacyDir(dir,prefix,filename,parent)
- if m:
- parent += m
-
-def __mergeLegacyDir(dir, prefix, filename, parent):
- dir = __check(dir,filename,"dir")
- if dir and dir not in tmp["DirectoryDirs"]:
- tmp["DirectoryDirs"].append(dir)
-
- m = Menu()
- m.AppDirs.append(dir)
- m.DirectoryDirs.append(dir)
- m.Name = os.path.basename(dir)
- m.NotInXml = True
-
- for item in os.listdir(dir):
- try:
- if item == ".directory":
- m.Directories.append(item)
- elif os.path.isdir(os.path.join(dir,item)):
- m.addSubmenu(__mergeLegacyDir(os.path.join(dir,item), prefix, filename, parent))
- except UnicodeDecodeError:
- continue
+ for menuentry in menuentries:
+ categories = menuentry.Categories
+ if len(categories) == 0:
+ r = Rule.fromFilename(Rule.TYPE_INCLUDE, menuentry.DesktopFileId)
+ m.rules.append(r)
+ if not dir_ in parent.AppDirs:
+ categories.append("Legacy")
+ menuentry.Categories = categories
- tmp["cache"].addMenuEntries([dir],prefix, True)
- menuentries = tmp["cache"].getMenuEntries([dir], False)
+ return m
- for menuentry in menuentries:
- categories = menuentry.Categories
- if len(categories) == 0:
- r = Rule.fromFilename(Rule.TYPE_INCLUDE, menuentry.DesktopFileID)
- m.Rules.append(r)
- if not dir in parent.AppDirs:
- categories.append("Legacy")
- menuentry.Categories = categories
-
- return m
-
-def __parseKDELegacyDirs(filename, parent):
- try:
- proc = subprocess.Popen(['kde-config', '--path', 'apps'],
- stdout=subprocess.PIPE, universal_newlines=True)
- output = proc.communicate()[0].splitlines()
- except OSError:
- # If kde-config doesn't exist, ignore this.
- return
-
- try:
- for dir in output[0].split(":"):
- __parseLegacyDir(dir,"kde", filename, parent)
- except IndexError:
- pass
-
-# ---------- <Rule> parsing
-
-def __parseRule(node):
- type = Rule.TYPE_INCLUDE if node.tagName == 'Include' else Rule.TYPE_EXCLUDE
- tree = ast.Expression(lineno=1, col_offset=0)
- expr = __parseBoolOp(node, ast.Or())
- if expr:
- tree.body = expr
- else:
- tree.body = ast.Name('False', ast.Load())
- ast.fix_missing_locations(tree)
- return Rule(type, tree)
-
-def __parseBoolOp(node, operator):
- values = []
- for child in _iter_children(node):
- rule = __parseRuleNode(child)
- if rule:
- values.append(rule)
- num_values = len(values)
- if num_values > 1:
- return ast.BoolOp(operator, values)
- elif num_values == 1:
- return values[0]
- return None
-
-def __parseRuleNode(node):
- tag = node.tagName
- if tag == 'Or':
- return __parseBoolOp(node, ast.Or())
- elif tag == 'And':
- return __parseBoolOp(node, ast.And())
- elif tag == 'Not':
- expr = __parseBoolOp(node, ast.Or())
- return ast.UnaryOp(ast.Not(), expr) if expr else None
- elif tag == 'All':
- return ast.Name('True', ast.Load())
- elif tag == 'Category':
- category = _get_node_text(node)
- return ast.Compare(
- left=ast.Str(category),
- ops=[ast.In()],
- comparators=[ast.Attribute(
- value=ast.Name(id='menuentry', ctx=ast.Load()),
- attr='Categories',
- ctx=ast.Load()
- )]
- )
- elif tag == 'Filename':
- filename = _get_node_text(node)
- return ast.Compare(
- left=ast.Str(filename),
- ops=[ast.Eq()],
- comparators=[ast.Attribute(
- value=ast.Name(id='menuentry', ctx=ast.Load()),
- attr='DesktopFileID',
- ctx=ast.Load()
- )]
- )
+ def parse_kde_legacy_dirs(self, filename, parent):
+ try:
+ proc = subprocess.Popen(
+ ['kde-config', '--path', 'apps'],
+ stdout=subprocess.PIPE,
+ universal_newlines=True
+ )
+ output = proc.communicate()[0].splitlines()
+ except OSError:
+ # If kde-config doesn't exist, ignore this.
+ return
+ try:
+ for dir_ in output[0].split(":"):
+ self.parse_legacy_dir(dir_, "kde", filename, parent)
+ except IndexError:
+ pass
-# remove duplicate entries from a list
-def __removeDuplicates(list):
- set = {}
- list.reverse()
- list = [set.setdefault(e,e) for e in list if e not in set]
- list.reverse()
- return list
-
-# Finally generate the menu
-def __genmenuNotOnlyAllocated(menu):
- for submenu in menu.Submenus:
- __genmenuNotOnlyAllocated(submenu)
-
- if menu.OnlyUnallocated == False:
- tmp["cache"].addMenuEntries(menu.AppDirs)
- menuentries = []
- for rule in menu.Rules:
- menuentries = rule.apply(tmp["cache"].getMenuEntries(menu.AppDirs), 1)
- for menuentry in menuentries:
- if menuentry.Add == True:
- menuentry.Parents.append(menu)
- menuentry.Add = False
- menuentry.Allocated = True
- menu.MenuEntries.append(menuentry)
-
-def __genmenuOnlyAllocated(menu):
- for submenu in menu.Submenus:
- __genmenuOnlyAllocated(submenu)
-
- if menu.OnlyUnallocated == True:
- tmp["cache"].addMenuEntries(menu.AppDirs)
- menuentries = []
- for rule in menu.Rules:
- menuentries = rule.apply(tmp["cache"].getMenuEntries(menu.AppDirs), 2)
- for menuentry in menuentries:
- if menuentry.Add == True:
- menuentry.Parents.append(menu)
- # menuentry.Add = False
- # menuentry.Allocated = True
- menu.MenuEntries.append(menuentry)
-
-# And sorting ...
-def sort(menu):
- menu.Entries = []
- menu.Visible = 0
-
- for submenu in menu.Submenus:
- sort(submenu)
-
- tmp_s = []
- tmp_e = []
-
- for order in menu.Layout.order:
- if order[0] == "Filename":
- tmp_e.append(order[1])
- elif order[0] == "Menuname":
- tmp_s.append(order[1])
-
- for order in menu.Layout.order:
- if order[0] == "Separator":
- separator = Separator(menu)
- if len(menu.Entries) > 0 and isinstance(menu.Entries[-1], Separator):
- separator.Show = False
- menu.Entries.append(separator)
- elif order[0] == "Filename":
- menuentry = menu.getMenuEntry(order[1])
- if menuentry:
- menu.Entries.append(menuentry)
- elif order[0] == "Menuname":
- submenu = menu.getMenu(order[1])
- if submenu:
- __parse_inline(submenu, menu)
- elif order[0] == "Merge":
- if order[1] == "files" or order[1] == "all":
- menu.MenuEntries.sort()
- for menuentry in menu.MenuEntries:
- if menuentry not in tmp_e:
- menu.Entries.append(menuentry)
- elif order[1] == "menus" or order[1] == "all":
- menu.Submenus.sort()
- for submenu in menu.Submenus:
- if submenu.Name not in tmp_s:
- __parse_inline(submenu, menu)
-
- # getHidden / NoDisplay / OnlyShowIn / NotOnlyShowIn / Deleted / NoExec
- for entry in menu.Entries:
- entry.Show = True
- menu.Visible += 1
- if isinstance(entry, Menu):
- if entry.Deleted == True:
- entry.Show = "Deleted"
- menu.Visible -= 1
- elif isinstance(entry.Directory, MenuEntry):
- if entry.Directory.DesktopEntry.getNoDisplay() == True:
- entry.Show = "NoDisplay"
- menu.Visible -= 1
- elif entry.Directory.DesktopEntry.getHidden() == True:
- entry.Show = "Hidden"
- menu.Visible -= 1
- elif isinstance(entry, MenuEntry):
- if entry.DesktopEntry.getNoDisplay() == True:
- entry.Show = "NoDisplay"
- menu.Visible -= 1
- elif entry.DesktopEntry.getHidden() == True:
- entry.Show = "Hidden"
- menu.Visible -= 1
- elif entry.DesktopEntry.getTryExec() and not __try_exec(entry.DesktopEntry.getTryExec()):
- entry.Show = "NoExec"
- menu.Visible -= 1
- elif xdg.Config.windowmanager:
- if ( entry.DesktopEntry.getOnlyShowIn() != [] and xdg.Config.windowmanager not in entry.DesktopEntry.getOnlyShowIn() ) \
- or xdg.Config.windowmanager in entry.DesktopEntry.getNotShowIn():
- entry.Show = "NotShowIn"
- menu.Visible -= 1
- elif isinstance(entry,Separator):
- menu.Visible -= 1
-
- # remove separators at the beginning and at the end
- if len(menu.Entries) > 0:
- if isinstance(menu.Entries[0], Separator):
- menu.Entries[0].Show = False
- if len(menu.Entries) > 1:
- if isinstance(menu.Entries[-1], Separator):
- menu.Entries[-1].Show = False
-
- # show_empty tag
- for entry in menu.Entries[:]:
- if isinstance(entry, Menu) and entry.Layout.show_empty == "false" and entry.Visible == 0:
- entry.Show = "Empty"
- menu.Visible -= 1
- if entry.NotInXml == True:
- menu.Entries.remove(entry)
-
-def __try_exec(executable):
- paths = os.environ['PATH'].split(os.pathsep)
- if not os.path.isfile(executable):
- for p in paths:
- f = os.path.join(p, executable)
- if os.path.isfile(f):
- if os.access(f, os.X_OK):
- return True
- else:
- if os.access(executable, os.X_OK):
- return True
- return False
+ def post_parse(self, menu):
+ # unallocated / deleted
+ if menu.Deleted == "notset":
+ menu.Deleted = False
+ if menu.OnlyUnallocated == "notset":
+ menu.OnlyUnallocated = False
+
+ # Layout Tags
+ if not menu.Layout or not menu.DefaultLayout:
+ if menu.DefaultLayout:
+ menu.Layout = menu.DefaultLayout
+ elif menu.Layout:
+ if menu.Depth > 0:
+ menu.DefaultLayout = menu.Parent.DefaultLayout
+ else:
+ menu.DefaultLayout = Layout()
+ else:
+ if menu.Depth > 0:
+ menu.Layout = menu.Parent.DefaultLayout
+ menu.DefaultLayout = menu.Parent.DefaultLayout
+ else:
+ menu.Layout = Layout()
+ menu.DefaultLayout = Layout()
+
+ # add parent's app/directory dirs
+ if menu.Depth > 0:
+ menu.AppDirs = menu.Parent.AppDirs + menu.AppDirs
+ menu.DirectoryDirs = menu.Parent.DirectoryDirs + menu.DirectoryDirs
+
+ # remove duplicates
+ menu.Directories = _dedupe(menu.Directories)
+ menu.DirectoryDirs = _dedupe(menu.DirectoryDirs)
+ menu.AppDirs = _dedupe(menu.AppDirs)
+
+ # go recursive through all menus
+ for submenu in menu.Submenus:
+ self.post_parse(submenu)
+
+ # reverse so handling is easier
+ menu.Directories.reverse()
+ menu.DirectoryDirs.reverse()
+ menu.AppDirs.reverse()
+
+ # get the valid .directory file out of the list
+ for directory in menu.Directories:
+ for dir in menu.DirectoryDirs:
+ if os.path.isfile(os.path.join(dir, directory)):
+ menuentry = MenuEntry(directory, dir)
+ if not menu.Directory:
+ menu.Directory = menuentry
+ elif menuentry.Type == MenuEntry.TYPE_SYSTEM:
+ if menu.Directory.Type == MenuEntry.TYPE_USER:
+ menu.Directory.Original = menuentry
+ if menu.Directory:
+ break
+
+ # Finally generate the menu
+ def generate_not_only_allocated(self, menu):
+ for submenu in menu.Submenus:
+ self.generate_not_only_allocated(submenu)
+
+ if menu.OnlyUnallocated is False:
+ self.cache.add_menu_entries(menu.AppDirs)
+ menuentries = []
+ for rule in menu.Rules:
+ menuentries = rule.apply(self.cache.get_menu_entries(menu.AppDirs), 1)
+
+ for menuentry in menuentries:
+ if menuentry.Add is True:
+ menuentry.Parents.append(menu)
+ menuentry.Add = False
+ menuentry.Allocated = True
+ menu.MenuEntries.append(menuentry)
+
+ def generate_only_allocated(self, menu):
+ for submenu in menu.Submenus:
+ self.generate_only_allocated(submenu)
+
+ if menu.OnlyUnallocated is True:
+ self.cache.add_menu_entries(menu.AppDirs)
+ menuentries = []
+ for rule in menu.Rules:
+ menuentries = rule.apply(self.cache.get_menu_entries(menu.AppDirs), 2)
+ for menuentry in menuentries:
+ if menuentry.Add is True:
+ menuentry.Parents.append(menu)
+ # menuentry.Add = False
+ # menuentry.Allocated = True
+ menu.MenuEntries.append(menuentry)
+
+ def handle_moves(self, menu):
+ for submenu in menu.Submenus:
+ self.handle_moves(submenu)
+ # parse move operations
+ for move in menu.Moves:
+ move_from_menu = menu.getMenu(move.Old)
+ if move_from_menu:
+ # FIXME: this is assigned, but never used...
+ move_to_menu = menu.getMenu(move.New)
+
+ menus = move.New.split("/")
+ oldparent = None
+ while len(menus) > 0:
+ if not oldparent:
+ oldparent = menu
+ newmenu = oldparent.getMenu(menus[0])
+ if not newmenu:
+ newmenu = Menu()
+ newmenu.Name = menus[0]
+ if len(menus) > 1:
+ newmenu.NotInXml = True
+ oldparent.addSubmenu(newmenu)
+ oldparent = newmenu
+ menus.pop(0)
+
+ newmenu += move_from_menu
+ move_from_menu.parent.Submenus.remove(move_from_menu)
-# inline tags
-def __parse_inline(submenu, menu):
- if submenu.Layout.inline == "true":
- if len(submenu.Entries) == 1 and submenu.Layout.inline_alias == "true":
- menuentry = submenu.Entries[0]
- menuentry.DesktopEntry.set("Name", submenu.getName(), locale = True)
- menuentry.DesktopEntry.set("GenericName", submenu.getGenericName(), locale = True)
- menuentry.DesktopEntry.set("Comment", submenu.getComment(), locale = True)
- menu.Entries.append(menuentry)
- elif len(submenu.Entries) <= submenu.Layout.inline_limit or submenu.Layout.inline_limit == 0:
- if submenu.Layout.inline_header == "true":
- header = Header(submenu.getName(), submenu.getGenericName(), submenu.getComment())
- menu.Entries.append(header)
- for entry in submenu.Entries:
- menu.Entries.append(entry)
- else:
- menu.Entries.append(submenu)
- else:
- menu.Entries.append(submenu)
class MenuEntryCache:
"Class to cache Desktop Entries"
@@ -1104,32 +1066,32 @@ class MenuEntryCache:
self.cacheEntries['legacy'] = []
self.cache = {}
- def addMenuEntries(self, dirs, prefix="", legacy=False):
- for dir in dirs:
- if not dir in self.cacheEntries:
- self.cacheEntries[dir] = []
- self.__addFiles(dir, "", prefix, legacy)
+ def add_menu_entries(self, dirs, prefix="", legacy=False):
+ for dir_ in dirs:
+ if not dir_ in self.cacheEntries:
+ self.cacheEntries[dir_] = []
+ self.__addFiles(dir_, "", prefix, legacy)
- def __addFiles(self, dir, subdir, prefix, legacy):
- for item in os.listdir(os.path.join(dir,subdir)):
- if os.path.splitext(item)[1] == ".desktop":
+ def __addFiles(self, dir_, subdir, prefix, legacy):
+ for item in os.listdir(os.path.join(dir_, subdir)):
+ if item.endswith(".desktop"):
try:
- menuentry = MenuEntry(os.path.join(subdir,item), dir, prefix)
+ menuentry = MenuEntry(os.path.join(subdir, item), dir_, prefix)
except ParsingError:
continue
- self.cacheEntries[dir].append(menuentry)
- if legacy == True:
+ self.cacheEntries[dir_].append(menuentry)
+ if legacy:
self.cacheEntries['legacy'].append(menuentry)
- elif os.path.isdir(os.path.join(dir,subdir,item)) and legacy == False:
- self.__addFiles(dir, os.path.join(subdir,item), prefix, legacy)
+ elif os.path.isdir(os.path.join(dir_, subdir, item)) and not legacy:
+ self.__addFiles(dir_, os.path.join(subdir, item), prefix, legacy)
- def getMenuEntries(self, dirs, legacy=True):
- list = []
- ids = []
+ def get_menu_entries(self, dirs, legacy=True):
+ entries = []
+ ids = set()
# handle legacy items
appdirs = dirs[:]
- if legacy == True:
+ if legacy:
appdirs.append("legacy")
# cache the results again
key = "".join(appdirs)
@@ -1137,19 +1099,26 @@ class MenuEntryCache:
return self.cache[key]
except KeyError:
pass
- for dir in appdirs:
- for menuentry in self.cacheEntries[dir]:
+ for dir_ in appdirs:
+ for menuentry in self.cacheEntries[dir_]:
try:
if menuentry.DesktopFileID not in ids:
- ids.append(menuentry.DesktopFileID)
- list.append(menuentry)
- elif menuentry.getType() == "System":
- # FIXME: This is only 99% correct, but still...
- i = list.index(menuentry)
- e = list[i]
- if e.getType() == "User":
- e.Original = menuentry
+ ids.add(menuentry.DesktopFileID)
+ entries.append(menuentry)
+ elif menuentry.getType() == MenuEntry.TYPE_SYSTEM:
+ # FIXME: This is only 99% correct, but still...
+ idx = entries.index(menuentry)
+ entry = entries[idx]
+ if entry.getType() == MenuEntry.TYPE_USER:
+ entry.Original = menuentry
except UnicodeDecodeError:
continue
- self.cache[key] = list
- return list
+ self.cache[key] = entries
+ return entries
+
+
+def parse(filename=None):
+ """Helper function.
+ Equivalent to calling xdg.Menu.Parser().parse(filename)
+ """
+ return Parser().parse(filename)
diff --git a/xdg/MenuEditor.py b/xdg/MenuEditor.py
index cc5ce54..0324f40 100644
--- a/xdg/MenuEditor.py
+++ b/xdg/MenuEditor.py
@@ -1,15 +1,17 @@
""" CLass to edit XDG Menus """
+import os
+try:
+ import xml.etree.cElementTree as etree
+except ImportError:
+ import xml.etree.ElementTree as etree
+#FIXME avoid importing all from all modules
from xdg.Menu import *
from xdg.BaseDirectory import *
from xdg.Exceptions import *
from xdg.DesktopEntry import *
from xdg.Config import *
-import xml.dom.minidom
-import os
-import re
-
# XML-Cleanups: Move / Exclude
# FIXME: proper reverte/delete
# FIXME: pass AppDirs/DirectoryDirs around in the edit/move functions
@@ -20,28 +22,31 @@ import re
# FIXME: Advanced MenuEditing Stuff: LegacyDir/MergeFile
# Complex Rules/Deleted/OnlyAllocated/AppDirs/DirectoryDirs
-class MenuEditor:
+
+class MenuEditor(object):
+
def __init__(self, menu=None, filename=None, root=False):
self.menu = None
self.filename = None
- self.doc = None
+ self.tree = None
+ self.parser = Parser()
self.parse(menu, filename, root)
# fix for creating two menus with the same name on the fly
self.filenames = []
def parse(self, menu=None, filename=None, root=False):
- if root == True:
+ if root:
setRootMode(True)
if isinstance(menu, Menu):
self.menu = menu
elif menu:
- self.menu = parse(menu)
+ self.menu = self.parser.parse(menu)
else:
- self.menu = parse()
+ self.menu = self.parser.parse()
- if root == True:
+ if root:
self.filename = self.menu.Filename
elif filename:
self.filename = filename
@@ -49,13 +54,21 @@ class MenuEditor:
self.filename = os.path.join(xdg_config_dirs[0], "menus", os.path.split(self.menu.Filename)[1])
try:
- self.doc = xml.dom.minidom.parse(self.filename)
+ self.tree = etree.parse(self.filename)
except IOError:
- self.doc = xml.dom.minidom.parseString('<!DOCTYPE Menu PUBLIC "-//freedesktop//DTD Menu 1.0//EN" "http://standards.freedesktop.org/menu-spec/menu-1.0.dtd"><Menu><Name>Applications</Name><MergeFile type="parent">'+self.menu.Filename+'</MergeFile></Menu>')
- except xml.parsers.expat.ExpatError:
+ root = etree.fromtring("""
+<!DOCTYPE Menu PUBLIC "-//freedesktop//DTD Menu 1.0//EN" "http://standards.freedesktop.org/menu-spec/menu-1.0.dtd">
+ <Menu>
+ <Name>Applications</Name>
+ <MergeFile type="parent">%s</MergeFile>
+ </Menu>
+""" % self.menu.Filename)
+ self.tree = etree.ElementTree(root)
+ except ParseError:
raise ParsingError('Not a valid .menu file', self.filename)
- self.__remove_whilespace_nodes(self.doc)
+ #FIXME: is this needed with etree ?
+ self.__remove_whitespace_nodes(self.tree)
def save(self):
self.__saveEntries(self.menu)
@@ -67,7 +80,7 @@ class MenuEditor:
self.__addEntry(parent, menuentry, after, before)
- sort(self.menu)
+ self.menu.sort()
return menuentry
@@ -83,7 +96,7 @@ class MenuEditor:
self.__addEntry(parent, menu, after, before)
- sort(self.menu)
+ self.menu.sort()
return menu
@@ -92,7 +105,7 @@ class MenuEditor:
self.__addEntry(parent, separator, after, before)
- sort(self.menu)
+ self.menu.sort()
return separator
@@ -100,7 +113,7 @@ class MenuEditor:
self.__deleteEntry(oldparent, menuentry, after, before)
self.__addEntry(newparent, menuentry, after, before)
- sort(self.menu)
+ self.menu.sort()
return menuentry
@@ -112,7 +125,7 @@ class MenuEditor:
if oldparent.getPath(True) != newparent.getPath(True):
self.__addXmlMove(root_menu, os.path.join(oldparent.getPath(True), menu.Name), os.path.join(newparent.getPath(True), menu.Name))
- sort(self.menu)
+ self.menu.sort()
return menu
@@ -120,14 +133,14 @@ class MenuEditor:
self.__deleteEntry(parent, separator, after, before)
self.__addEntry(parent, separator, after, before)
- sort(self.menu)
+ self.menu.sort()
return separator
def copyMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None):
self.__addEntry(newparent, menuentry, after, before)
- sort(self.menu)
+ self.menu.sort()
return menuentry
@@ -137,39 +150,39 @@ class MenuEditor:
if name:
if not deskentry.hasKey("Name"):
deskentry.set("Name", name)
- deskentry.set("Name", name, locale = True)
+ deskentry.set("Name", name, locale=True)
if comment:
if not deskentry.hasKey("Comment"):
deskentry.set("Comment", comment)
- deskentry.set("Comment", comment, locale = True)
+ deskentry.set("Comment", comment, locale=True)
if genericname:
- if not deskentry.hasKey("GnericNe"):
+ if not deskentry.hasKey("GenericName"):
deskentry.set("GenericName", genericname)
- deskentry.set("GenericName", genericname, locale = True)
+ deskentry.set("GenericName", genericname, locale=True)
if command:
deskentry.set("Exec", command)
if icon:
deskentry.set("Icon", icon)
- if terminal == True:
+ if terminal:
deskentry.set("Terminal", "true")
- elif terminal == False:
+ elif not terminal:
deskentry.set("Terminal", "false")
- if nodisplay == True:
+ if nodisplay is True:
deskentry.set("NoDisplay", "true")
- elif nodisplay == False:
+ elif nodisplay is False:
deskentry.set("NoDisplay", "false")
- if hidden == True:
+ if hidden is True:
deskentry.set("Hidden", "true")
- elif hidden == False:
+ elif hidden is False:
deskentry.set("Hidden", "false")
menuentry.updateAttributes()
if len(menuentry.Parents) > 0:
- sort(self.menu)
+ self.menu.sort()
return menuentry
@@ -195,56 +208,58 @@ class MenuEditor:
if name:
if not deskentry.hasKey("Name"):
deskentry.set("Name", name)
- deskentry.set("Name", name, locale = True)
+ deskentry.set("Name", name, locale=True)
if genericname:
if not deskentry.hasKey("GenericName"):
deskentry.set("GenericName", genericname)
- deskentry.set("GenericName", genericname, locale = True)
+ deskentry.set("GenericName", genericname, locale=True)
if comment:
if not deskentry.hasKey("Comment"):
deskentry.set("Comment", comment)
- deskentry.set("Comment", comment, locale = True)
+ deskentry.set("Comment", comment, locale=True)
if icon:
deskentry.set("Icon", icon)
- if nodisplay == True:
+ if nodisplay is True:
deskentry.set("NoDisplay", "true")
- elif nodisplay == False:
+ elif nodisplay is False:
deskentry.set("NoDisplay", "false")
- if hidden == True:
+ if hidden is True:
deskentry.set("Hidden", "true")
- elif hidden == False:
+ elif hidden is False:
deskentry.set("Hidden", "false")
menu.Directory.updateAttributes()
if isinstance(menu.Parent, Menu):
- sort(self.menu)
+ self.menu.sort()
return menu
def hideMenuEntry(self, menuentry):
- self.editMenuEntry(menuentry, nodisplay = True)
+ self.editMenuEntry(menuentry, nodisplay=True)
def unhideMenuEntry(self, menuentry):
- self.editMenuEntry(menuentry, nodisplay = False, hidden = False)
+ self.editMenuEntry(menuentry, nodisplay=False, hidden=False)
def hideMenu(self, menu):
- self.editMenu(menu, nodisplay = True)
+ self.editMenu(menu, nodisplay=True)
def unhideMenu(self, menu):
- self.editMenu(menu, nodisplay = False, hidden = False)
- xml_menu = self.__getXmlMenu(menu.getPath(True,True), False)
- for node in self.__getXmlNodesByName(["Deleted", "NotDeleted"], xml_menu):
- node.parentNode.removeChild(node)
+ self.editMenu(menu, nodisplay=False, hidden=False)
+ xml_menu = self.__getXmlMenu(menu.getPath(True, True), False)
+ deleted = xml_menu.findall('Deleted')
+ not_deleted = xml_menu.findall('NotDeleted')
+ for node in deleted + not_deleted:
+ xml_menu.remove(node)
def deleteMenuEntry(self, menuentry):
if self.getAction(menuentry) == "delete":
self.__deleteFile(menuentry.DesktopEntry.filename)
for parent in menuentry.Parents:
self.__deleteEntry(parent, menuentry)
- sort(self.menu)
+ self.menu.sort()
return menuentry
def revertMenuEntry(self, menuentry):
@@ -257,7 +272,7 @@ class MenuEditor:
index = parent.MenuEntries.index(menuentry)
parent.MenuEntries[index] = menuentry.Original
menuentry.Original.Parents.append(parent)
- sort(self.menu)
+ self.menu.sort()
return menuentry
def deleteMenu(self, menu):
@@ -265,21 +280,22 @@ class MenuEditor:
self.__deleteFile(menu.Directory.DesktopEntry.filename)
self.__deleteEntry(menu.Parent, menu)
xml_menu = self.__getXmlMenu(menu.getPath(True, True))
- xml_menu.parentNode.removeChild(xml_menu)
- sort(self.menu)
+ parent = self.__get_parent_node(xml_menu)
+ parent.remove(xml_menu)
+ self.menu.sort()
return menu
def revertMenu(self, menu):
if self.getAction(menu) == "revert":
self.__deleteFile(menu.Directory.DesktopEntry.filename)
menu.Directory = menu.Directory.Original
- sort(self.menu)
+ self.menu.sort()
return menu
def deleteSeparator(self, separator):
self.__deleteEntry(separator.Parent, separator, after=True)
- sort(self.menu)
+ self.menu.sort()
return separator
@@ -290,8 +306,9 @@ class MenuEditor:
return "none"
elif entry.Directory.getType() == "Both":
return "revert"
- elif entry.Directory.getType() == "User" \
- and (len(entry.Submenus) + len(entry.MenuEntries)) == 0:
+ elif entry.Directory.getType() == "User" and (
+ len(entry.Submenus) + len(entry.MenuEntries)
+ ) == 0:
return "delete"
elif isinstance(entry, MenuEntry):
@@ -318,9 +335,7 @@ class MenuEditor:
def __saveMenu(self):
if not os.path.isdir(os.path.dirname(self.filename)):
os.makedirs(os.path.dirname(self.filename))
- fd = open(self.filename, 'w')
- fd.write(re.sub("\n[\s]*([^\n<]*)\n[\s]*</", "\\1</", self.doc.toprettyxml().replace('<?xml version="1.0" ?>\n', '')))
- fd.close()
+ self.tree.write(self.filename, encoding='utf-8')
def __getFileName(self, name, extension):
postfix = 0
@@ -333,8 +348,9 @@ class MenuEditor:
dir = "applications"
elif extension == ".directory":
dir = "desktop-directories"
- if not filename in self.filenames and not \
- os.path.isfile(os.path.join(xdg_data_dirs[0], dir, filename)):
+ if not filename in self.filenames and not os.path.isfile(
+ os.path.join(xdg_data_dirs[0], dir, filename)
+ ):
self.filenames.append(filename)
break
else:
@@ -343,8 +359,11 @@ class MenuEditor:
return filename
def __getXmlMenu(self, path, create=True, element=None):
+ # FIXME: we should also return the menu's parent,
+ # to avoid looking for it later on
+ # @see Element.getiterator()
if not element:
- element = self.doc
+ element = self.tree
if "/" in path:
(name, path) = path.split("/", 1)
@@ -353,17 +372,16 @@ class MenuEditor:
path = ""
found = None
- for node in self.__getXmlNodesByName("Menu", element):
- for child in self.__getXmlNodesByName("Name", node):
- if child.childNodes[0].nodeValue == name:
- if path:
- found = self.__getXmlMenu(path, create, node)
- else:
- found = node
- break
+ for node in element.findall("Menu"):
+ name_node = node.find('Name')
+ if name_node.text == name:
+ if path:
+ found = self.__getXmlMenu(path, create, node)
+ else:
+ found = node
if found:
break
- if not found and create == True:
+ if not found and create:
node = self.__addXmlMenuElement(element, name)
if path:
found = self.__getXmlMenu(path, create, node)
@@ -373,58 +391,62 @@ class MenuEditor:
return found
def __addXmlMenuElement(self, element, name):
- node = self.doc.createElement('Menu')
- self.__addXmlTextElement(node, 'Name', name)
- return element.appendChild(node)
+ menu_node = etree.SubElement('Menu', element)
+ name_node = etree.SubElement('Name', menu_node)
+ name_node.text = name
+ return menu_node
def __addXmlTextElement(self, element, name, text):
- node = self.doc.createElement(name)
- text = self.doc.createTextNode(text)
- node.appendChild(text)
- return element.appendChild(node)
+ node = etree.SubElement(name, element)
+ node.text = text
+ return node
- def __addXmlFilename(self, element, filename, type = "Include"):
+ def __addXmlFilename(self, element, filename, type_="Include"):
# remove old filenames
- for node in self.__getXmlNodesByName(["Include", "Exclude"], element):
- if node.childNodes[0].nodeName == "Filename" and node.childNodes[0].childNodes[0].nodeValue == filename:
- element.removeChild(node)
+ includes = element.findall('Include')
+ excludes = element.findall('Exclude')
+ rules = includes + excludes
+ for rule in rules:
+ #FIXME: this finds only Rules whose FIRST child is a Filename element
+ if rule[0].tag == "Filename" and rule[0].text == filename:
+ element.remove(rule)
+ # shouldn't it remove all occurences, like the following:
+ #filename_nodes = rule.findall('.//Filename'):
+ #for fn in filename_nodes:
+ #if fn.text == filename:
+ ##element.remove(rule)
+ #parent = self.__get_parent_node(fn)
+ #parent.remove(fn)
# add new filename
- node = self.doc.createElement(type)
- node.appendChild(self.__addXmlTextElement(node, 'Filename', filename))
- return element.appendChild(node)
+ node = etree.SubElement(type_, element)
+ self.__addXmlTextElement(node, 'Filename', filename)
+ return node
def __addXmlMove(self, element, old, new):
- node = self.doc.createElement("Move")
- node.appendChild(self.__addXmlTextElement(node, 'Old', old))
- node.appendChild(self.__addXmlTextElement(node, 'New', new))
- return element.appendChild(node)
+ node = etree.SubElement("Move", element)
+ self.__addXmlTextElement(node, 'Old', old)
+ self.__addXmlTextElement(node, 'New', new)
+ return node
def __addXmlLayout(self, element, layout):
# remove old layout
- for node in self.__getXmlNodesByName("Layout", element):
- element.removeChild(node)
+ for node in element.findall("Layout"):
+ element.remove(node)
# add new layout
- node = self.doc.createElement("Layout")
+ node = etree.SubElement("Layout", element)
for order in layout.order:
if order[0] == "Separator":
- child = self.doc.createElement("Separator")
- node.appendChild(child)
+ child = etree.SubElement("Separator", node)
elif order[0] == "Filename":
child = self.__addXmlTextElement(node, "Filename", order[1])
elif order[0] == "Menuname":
child = self.__addXmlTextElement(node, "Menuname", order[1])
elif order[0] == "Merge":
- child = self.doc.createElement("Merge")
- child.setAttribute("type", order[1])
- node.appendChild(child)
- return element.appendChild(node)
-
- def __getXmlNodesByName(self, name, element):
- for child in element.childNodes:
- if child.nodeType == xml.dom.Node.ELEMENT_NODE and child.nodeName in name:
- yield child
+ child = etree.SubElement("Merge", node)
+ child.attrib["type"] = order[1]
+ return node
def __addLayout(self, parent):
layout = Layout()
@@ -498,14 +520,24 @@ class MenuEditor:
except ValueError:
pass
- def __remove_whilespace_nodes(self, node):
- remove_list = []
- for child in node.childNodes:
- if child.nodeType == xml.dom.minidom.Node.TEXT_NODE:
- child.data = child.data.strip()
- if not child.data.strip():
- remove_list.append(child)
- elif child.hasChildNodes():
+ def __remove_whitespace_nodes(self, node):
+ for child in node:
+ text = child.text.strip()
+ if not text:
+ child.text = ''
+ tail = child.tail.strip()
+ if not tail:
+ child.tail = ''
+ if len(child):
self.__remove_whilespace_nodes(child)
- for node in remove_list:
- node.parentNode.removeChild(node)
+
+ def __get_parent_node(self, node):
+ # elements in ElementTree doesn't hold a reference to their parent
+ for parent, child in self.__iter_parent():
+ if child is node:
+ return child
+
+ def __iter_parent(self):
+ for parent in self.tree.getiterator():
+ for child in parent:
+ yield parent, child